@genlobe/mcp-server 3.4.0 → 3.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +22 -4
  2. package/dist/index.js +667 -129
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -74,6 +74,7 @@ Edit `claude_desktop_config.json`:
74
74
  |---|---|---|
75
75
  | `SAAS_API_URL` | no | Defaults to `https://api-core.genlobe.ai`. Override only if you point at a local or on-prem backend. |
76
76
  | `SAAS_API_KEY` | optional | Your Genlobe API key. Accepts both `pk_live_*` (public) and `sk_live_*` (secret). Without it, only the offline documentation tools work — the four live tools (`validate_credentials`, `list_end_users`, `list_oauth_providers`, `get_openapi_spec`) need it. |
77
+ | `SAAS_FRONTEND_URL` | no | Dashboard URL used by the redirect-URL tools (`get_stripe_config_url`, `get_agent_secrets_url`, `get_plan_management_url`). Resolution order: (1) this variable if set; (2) derived from `SAAS_API_URL` by swapping the leading `api.` / `api-core.` sub-domain for `app.`; (3) `https://app.genlobe.ai`. |
77
78
 
78
79
  `API_URL` and `API_KEY` (without the `SAAS_` prefix) are accepted as aliases for backward compatibility.
79
80
 
@@ -81,14 +82,16 @@ Edit `claude_desktop_config.json`:
81
82
 
82
83
  ## Tools
83
84
 
84
- 14 tools, in three groups.
85
+ 19 tools, in four groups.
85
86
 
86
87
  ### Documentation (offline, no API key needed)
87
88
 
88
89
  | Tool | Returns |
89
90
  |---|---|
90
91
  | `get_api_overview` | High-level architecture, auth model, key concepts, recommended call order. **Start here.** |
91
- | `get_end_user_endpoints` | Reference for every end-user endpoint, filterable by category (`authentication`, `organizations`, `subscriptions`, `billing`, `usage`, `agents`, `users`, `plans`, `ai`, `entities`, `departments`, `files`, `external_agents`). |
92
+ | `list_endpoint_categories` | Bare list of end-user endpoint categories plus a one-line summary of each. ~30 tokens. Call this once when you don't already know what's available; then use `get_end_user_endpoints({ category })` for the actual docs. |
93
+ | `get_end_user_endpoints` | Reference for end-user endpoints in ONE category. Pass `category` (one of `authentication`, `organizations`, `subscriptions`, `billing`, `organization_admin`, `usage`, `agents`, `users`, `plans`, `ai`, `entities`, `departments`, `files`, `external_agents`). The `billing` category covers Tier-3 admin read-only billing endpoints (`/v1/organization-admin/{org_id}/{plans, config/stripe, audit, subscriptions}`); the new `organization_admin` category covers the non-billing Org Owner Dashboard surfaces (members, customers, agents, knowledge bases, agent-change notifications, Stripe webhook audit). Calling without a category returns the full catalog — use sparingly. |
94
+ | `get_reserved_schema_slugs` | Returns the 53 slugs the Custom Entities subsystem refuses at `POST /v1/entity/schemas` (ADR-0011 — they collide with native DB tables: `users`, `conversations`, `agents`, …). Pure data, no network call. Use it to pre-validate before issuing the POST and avoid a `400` round-trip. |
92
95
  | `get_request_headers` | The exact HTTP headers required for end-user requests, with examples. |
93
96
  | `get_authentication_flow` | Step-by-step register → login → refresh → logout, including token storage and refresh-on-401 patterns. |
94
97
  | `get_common_patterns` | Pagination, error handling, optimistic updates, file upload, RAG queries, agent execution. |
@@ -96,6 +99,18 @@ Edit `claude_desktop_config.json`:
96
99
  | `search_endpoints` | Keyword search across path, summary, and description. |
97
100
  | `get_endpoint_details` | Full request/response schema for a specific `(method, path)`. |
98
101
 
102
+ ### Redirect-URL tools (no secrets ever returned by the MCP)
103
+
104
+ These tools return a Dashboard URL the human developer should visit to set or rotate a secret. The MCP NEVER accepts or returns raw secret values; instead it points the agent at the right page so the human pastes the key in the browser. See the "MCP server invariants" section in the root `CLAUDE.md` for the pattern.
105
+
106
+ All three return `{ url, reason, next_action: { type: "open_in_browser", url } }` and do no network calls — they are pure URL computation against the configured `SAAS_FRONTEND_URL`.
107
+
108
+ | Tool | Returns |
109
+ |---|---|
110
+ | `get_stripe_config_url({ org_id })` | `${frontend_url}/org-admin/{org_id}/integrations/stripe` — page where the Organization Owner sets / rotates the org's Stripe `secret_key`, `publishable_key`, `webhook_secret`. Pair it with `GET /v1/organization-admin/{org_id}/config/stripe` (returned masked) to detect whether Stripe is already configured. |
111
+ | `get_agent_secrets_url({ agent_id })` | `${frontend_url}/dashboard/agents/{agent_id}/secrets` — page where the Tenant rotates / sets an Agent's provider keys (OpenAI / Anthropic / etc.) and webhook secrets. |
112
+ | `get_plan_management_url({ org_id })` | `${frontend_url}/org-admin/{org_id}/plans` — page where the Organization Owner creates / edits / deletes Plans. Write operations are not exposed via MCP because the plan-sync flow needs the encrypted Stripe `secret_key`. |
113
+
99
114
  ### Live (require `SAAS_API_KEY`)
100
115
 
101
116
  | Tool | Does |
@@ -122,12 +137,15 @@ Edit `claude_desktop_config.json`:
122
137
  3. recommend_stack → pick a framework that won't leak the key
123
138
  4. get_api_overview → understand the API at a high level
124
139
  5. get_authentication_flow → wire up register / login / refresh
125
- 6. get_end_user_endpoints → see what's available for the feature you're building
126
- 7. get_sdk_template start writing real code
140
+ 6. list_endpoint_categories → see which endpoint categories exist
141
+ 7. get_end_user_endpoints drill into one category at a time
142
+ 8. get_sdk_template → start writing real code
127
143
  ```
128
144
 
129
145
  For targeted questions ("how do I list organizations?"), `search_endpoints` + `get_endpoint_details` is faster than scrolling through `get_end_user_endpoints`.
130
146
 
147
+ When the developer needs to set or rotate a secret (Stripe key, Agent provider key, webhook secret), call the matching `get_*_url` tool — the MCP returns a Dashboard URL the human visits to paste the key. Inline secret input through the MCP is not supported by design.
148
+
131
149
  ---
132
150
 
133
151
  ## Support
package/dist/index.js CHANGED
@@ -36,6 +36,45 @@ const SERVER_VERSION = pkg.version;
36
36
  // a local backend in development.
37
37
  const API_URL = process.env.SAAS_API_URL || process.env.API_URL || "https://api-core.genlobe.ai";
38
38
  const API_KEY = process.env.SAAS_API_KEY || process.env.API_KEY || "";
39
+ // Frontend URL used by the redirect-URL tools (get_stripe_config_url,
40
+ // get_agent_secrets_url, get_plan_management_url). Resolution order:
41
+ // 1. SAAS_FRONTEND_URL env var (e.g. https://app.genlobe.ai)
42
+ // 2. Derive from SAAS_API_URL by swapping the leading "api." / "api-core."
43
+ // sub-domain for "app." — works for the public prod + staging
44
+ // environments out of the box.
45
+ // 3. Default https://app.genlobe.ai.
46
+ //
47
+ // The MCP NEVER returns secret values to the agent; instead it returns
48
+ // these URLs so the human developer can paste the secret in the Dashboard.
49
+ // See "MCP server invariants" in the root CLAUDE.md.
50
+ function resolveFrontendUrl() {
51
+ const explicit = process.env.SAAS_FRONTEND_URL;
52
+ if (explicit)
53
+ return explicit.replace(/\/+$/, "");
54
+ const derived = API_URL.replace(/^(https?:\/\/)(api-core\.|api\.)/, "$1app.");
55
+ if (derived !== API_URL)
56
+ return derived.replace(/\/+$/, "");
57
+ // Localhost API → keep the convention of port 3000/3001 for the dashboards
58
+ // by NOT pretending to know which one. Fall back to the documented default.
59
+ return "https://app.genlobe.ai";
60
+ }
61
+ const FRONTEND_URL = resolveFrontendUrl();
62
+ // Canonical structured rejection payload for any future tool that might
63
+ // receive secret-shaped input. The MCP NEVER accepts raw secrets inline —
64
+ // the agent must follow `next_action.url` and the human pastes the secret
65
+ // in the dashboard. Documented in the root CLAUDE.md "MCP server invariants".
66
+ //
67
+ // Not wired into any current tool (none of them accept secret-shaped input
68
+ // today), but kept here so future maintainers have a single canonical
69
+ // shape to reuse rather than re-invent.
70
+ function rejectSecretInline(secretType, reason, redirectUrl) {
71
+ return {
72
+ error: "blocked_by_security_policy",
73
+ reason,
74
+ secret_type: secretType,
75
+ next_action: { type: "redirect", url: redirectUrl },
76
+ };
77
+ }
39
78
  // Cache for API responses
40
79
  const cache = new Map();
41
80
  const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
@@ -153,6 +192,82 @@ The \`sk_live_*\` key never reaches the browser. It lives only on the tenant's o
153
192
  }
154
193
  };
155
194
  // =============================================================================
195
+ // Reserved schema slugs (ADR-0011)
196
+ // =============================================================================
197
+ //
198
+ // Mirrors `RESERVED_SCHEMA_SLUGS` in
199
+ // `src/tenants/services/custom_data_schema_service.py`. The backend is the
200
+ // authoritative source — this list is documentation-only and is exposed
201
+ // through the `get_reserved_schema_slugs` tool so agents can pre-validate
202
+ // before POSTing to /v1/entity/schemas.
203
+ //
204
+ // When the backend list changes (a new native __tablename__ is added),
205
+ // this constant MUST be updated in the same MCP PR.
206
+ const RESERVED_SCHEMA_SLUGS = Object.freeze([
207
+ // --- Alembic / Postgres internals -------------------------------------
208
+ "alembic_version",
209
+ // --- Tenants domain ---------------------------------------------------
210
+ "tenants",
211
+ "tenant_members",
212
+ "tenant_configs",
213
+ "tenant_password_reset_tokens",
214
+ "api_keys",
215
+ "email_logs",
216
+ "email_domains",
217
+ "email_verification_codes",
218
+ "custom_data_schemas",
219
+ "custom_data_records",
220
+ // --- Organizations domain --------------------------------------------
221
+ "organizations",
222
+ "organization_members",
223
+ "organization_configs",
224
+ "organization_invitations",
225
+ "departments",
226
+ "permissions",
227
+ "role_permissions",
228
+ // --- Users / Auth -----------------------------------------------------
229
+ "users",
230
+ "user_settings",
231
+ "password_reset_tokens",
232
+ // --- Subscriptions / Billing -----------------------------------------
233
+ "plans",
234
+ "tenant_plans",
235
+ "organization_plans",
236
+ "subscriptions",
237
+ "tenant_subscriptions",
238
+ "tenant_usages",
239
+ "tenant_usage_details",
240
+ "organization_usage_details",
241
+ "plan_usages",
242
+ "usage_logs",
243
+ "billing_audit_log",
244
+ "billing_meters",
245
+ "processed_stripe_events",
246
+ // --- AI domain --------------------------------------------------------
247
+ "agents",
248
+ "agent_executions",
249
+ "agent_tools",
250
+ "tool_executions",
251
+ "tool_secrets",
252
+ "external_agents",
253
+ "external_agent_calls",
254
+ "conversations",
255
+ "conversation_messages",
256
+ "knowledge_bases",
257
+ "documents",
258
+ "document_chunks",
259
+ // --- Files / Drive ---------------------------------------------------
260
+ "tenant_files",
261
+ "user_files",
262
+ "folders",
263
+ // --- Workflows (no Python model; defined only in migration) ---------
264
+ "workflows",
265
+ "workflow_executions",
266
+ "node_executions",
267
+ // --- Admin domain ----------------------------------------------------
268
+ "admin_audit_logs",
269
+ ]);
270
+ // =============================================================================
156
271
  // End-User Endpoints (for building frontends)
157
272
  // =============================================================================
158
273
  const END_USER_ENDPOINTS = {
@@ -163,7 +278,7 @@ const END_USER_ENDPOINTS = {
163
278
  method: "POST",
164
279
  path: "/v1/auth/register",
165
280
  summary: "Register a new user",
166
- auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
281
+ auth: { api_key: "pk_live_* or sk_live_* (REQUIRED)", jwt: false, headers: "X-Organization-Id required when the API key is root-scoped (org_id NULL on the key). Ignored when the key is already scoped to a specific Organization — the key wins." },
167
282
  rate_limit: "3 per hour per IP (blocked 30 min after exceeding)",
168
283
  request_body: {
169
284
  email: "string (required) - valid email",
@@ -184,7 +299,8 @@ const END_USER_ENDPOINTS = {
184
299
  display_name: "string | null",
185
300
  avatar_url: "string | null",
186
301
  is_active: "boolean",
187
- created_at: "ISO datetime"
302
+ created_at: "ISO datetime",
303
+ profile_data: "object — Tenant-defined free-form metadata for this user. Defaults to {} when empty (never null). Mutate via PATCH /v1/users/me/profile-data. ADR-0010."
188
304
  }
189
305
  },
190
306
  response_when_verification_required: {
@@ -236,7 +352,8 @@ const END_USER_ENDPOINTS = {
236
352
  display_name: "string | null",
237
353
  avatar_url: "string | null",
238
354
  is_active: "boolean",
239
- created_at: "ISO datetime"
355
+ created_at: "ISO datetime",
356
+ profile_data: "object — Tenant-defined free-form metadata. Defaults to {} when empty (never null). ADR-0010."
240
357
  }
241
358
  },
242
359
  {
@@ -829,7 +946,7 @@ const END_USER_ENDPOINTS = {
829
946
  ]
830
947
  },
831
948
  billing: {
832
- description: "Billing operations. Two flavors:\n\n- **Tier 1 (legacy / platform-level)**: endpoints under /v1/billing/* use the platform's Stripe and a SECRET key (sk_live_*) only. They were originally for Genlobe → Tenant billing and a since-superseded Tier 2 prototype; keep using them for tenant-internal flows.\n\n- **Tier 3 (Organization end-user, Epic #209)**: each Organization runs its OWN Stripe (BYO key). End-users subscribe via /v1/organizations/{org_id}/billing/* (api key + JWT + active OrganizationMember). The Org's plan catalog is read via the public /v1/organizations/{org_id}/plans (api key only — no JWT). See the master spec at specs/features/billing/three-tier-billing.spec.md.",
949
+ description: "Billing operations. Three flavors:\n\n- **Tier 1 (legacy / platform-level)**: endpoints under /v1/billing/* use the platform's Stripe and a SECRET key (sk_live_*) only. They were originally for Genlobe → Tenant billing and a since-superseded Tier 2 prototype; keep using them for tenant-internal flows.\n\n- **Tier 3 end-user surface (Epic #209)**: each Organization runs its OWN Stripe (BYO key). End-users subscribe via /v1/organizations/{org_id}/billing/* (api key + JWT + active OrganizationMember). The Org's plan catalog is read via the public /v1/organizations/{org_id}/plans (api key only — no JWT). See the master spec at specs/features/billing/three-tier-billing.spec.md.\n\n- **Tier 3 — Organization-admin surface (Phase 1, this MCP)**: /v1/organization-admin/{org_id}/* — read-only inspection of plans, masked Stripe config, and billing audit log. Auth: end-user JWT + `require_org_admin` (role IN owner/admin). Secret-setting paths (POST/PATCH/DELETE on config/stripe, plan CRUD) are NOT exposed via MCP — use the `get_stripe_config_url({ org_id })` / `get_plan_management_url({ org_id })` tools so the human pastes secrets in the Dashboard.",
833
950
  endpoints: [
834
951
  {
835
952
  method: "GET",
@@ -1007,6 +1124,269 @@ const END_USER_ENDPOINTS = {
1007
1124
  "403": "Caller is not an active OrganizationMember of the path's org.",
1008
1125
  "404": "Caller has no Subscription for this Organization yet."
1009
1126
  }
1127
+ },
1128
+ // ---------------------------------------------------------------
1129
+ // Organization-admin read-only endpoints (BILL-4 / BILL-9, Epic
1130
+ // #209). Auth: end-user JWT + `require_org_admin` (the caller must
1131
+ // be an OrganizationMember of the path's org with role IN ('owner',
1132
+ // 'admin')). These are the surfaces an Org Owner uses to inspect
1133
+ // billing state from the MCP without touching any secret. The
1134
+ // secret-setting paths (POST/PATCH/DELETE on config/stripe + plan
1135
+ // CRUD that needs a Stripe key) are intentionally NOT exposed here
1136
+ // — they go through the redirect-URL tools (`get_stripe_config_url`,
1137
+ // `get_plan_management_url`) so the human pastes the secret in the
1138
+ // Dashboard. See "MCP server invariants" in the root CLAUDE.md.
1139
+ // ---------------------------------------------------------------
1140
+ {
1141
+ method: "GET",
1142
+ path: "/v1/organization-admin/{organization_id}/plans",
1143
+ summary: "List the Organization's plans (admin view — includes inactive / un-synced plans)",
1144
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1145
+ response: {
1146
+ items: "array of OrganizationPlan (see GET /v1/organizations/{organization_id}/plans for the field list — same shape). Unlike the public endpoint this one also returns plans with is_active=false and plans that don't have a stripe_price_id yet.",
1147
+ total: "number"
1148
+ },
1149
+ errors: {
1150
+ "403": "Caller is not an OrganizationMember with role owner/admin of the path's org (rejects TenantMember JWT)."
1151
+ },
1152
+ note: "The admin variant of GET /v1/organizations/{organization_id}/plans. Use this when an Org Owner needs to see every plan they have, including drafts. No secret material is returned."
1153
+ },
1154
+ {
1155
+ method: "GET",
1156
+ path: "/v1/organization-admin/{organization_id}/plans/{plan_id}",
1157
+ summary: "Read one plan (admin view)",
1158
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1159
+ response: "OrganizationPlan (full shape — id, organization_id, name, slug, description, price_amount, currency, billing_interval, features, limits, stripe_product_id, stripe_price_id, stripe_price_id_yearly, is_active, created_at, updated_at).",
1160
+ errors: {
1161
+ "403": "Caller is not an OrganizationMember with role owner/admin of the path's org.",
1162
+ "404": "Plan not found OR plan.organization_id != path organization_id (collapsed to 404 to avoid cross-org existence leaks)."
1163
+ }
1164
+ },
1165
+ {
1166
+ method: "GET",
1167
+ path: "/v1/organization-admin/{organization_id}/config/stripe",
1168
+ summary: "Read the Organization's MASKED Stripe config status (no secrets returned)",
1169
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1170
+ response: {
1171
+ organization_id: "uuid",
1172
+ publishable_key: "string | null — Stripe publishable key (pk_live_* / pk_test_*), safe to expose.",
1173
+ secret_key_display: "string | null — masked tail of the secret key, e.g. 'sk_live_****Pnxs'. Display-only — the actual key is never returned.",
1174
+ has_webhook_secret: "boolean — true if a webhook_secret is persisted.",
1175
+ is_active: "boolean — true once secret_key + webhook_secret are both present."
1176
+ },
1177
+ note: "Use this to detect whether the Org has Stripe configured before sending users into a checkout flow that would 409. To SET or ROTATE the Stripe credentials, call the MCP tool `get_stripe_config_url({ org_id })` and have the human paste the keys in the Dashboard — the MCP does NOT accept raw secrets inline.",
1178
+ errors: {
1179
+ "403": "Caller is not an OrganizationMember with role owner/admin of the path's org."
1180
+ }
1181
+ },
1182
+ // ---------------------------------------------------------------
1183
+ // Org-admin Subscription list (Tier-3, end-user subscriptions view).
1184
+ // Distinct from the END-USER read at GET /v1/organizations/{id}/billing/subscription
1185
+ // (which returns ONLY the caller's own subscription). This one is
1186
+ // the dashboard's "list of paying customers" surface.
1187
+ // ---------------------------------------------------------------
1188
+ {
1189
+ method: "GET",
1190
+ path: "/v1/organization-admin/{organization_id}/subscriptions",
1191
+ summary: "List end-user subscriptions for the Organization (admin)",
1192
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1193
+ query_params: {
1194
+ status: "string (optional) — comma-separated subscription statuses to filter (e.g. 'active,trialing,past_due'). Omit for all."
1195
+ },
1196
+ response: {
1197
+ items: "array of SubscriptionResponse — every Subscription row scoped to this Organization, sorted newest-first.",
1198
+ total: "number"
1199
+ },
1200
+ note: "Active statuses for the 'paying customers' count are: active, trialing, past_due. canceled / incomplete_expired / stripe_disconnected do NOT count as active.",
1201
+ errors: {
1202
+ "403": "Caller is not an OrganizationMember with role owner/admin of the path's org."
1203
+ }
1204
+ }
1205
+ ]
1206
+ },
1207
+ organization_admin: {
1208
+ description: "Organization Owner / Admin surface that an agent uses while BUILDING the Tenant's product — team management (`members`), AI resource management (`agents` visibility, `knowledge-bases` + `documents` CRUD). Excludes observability endpoints (audit log, webhook delivery history, admin notifications, customer CRM list) — those live in the dashboards for humans, not in the MCP. Auth pattern across all of these: pk_live_*/sk_live_* + end-user JWT + `require_org_admin` (OrganizationMember.role IN owner/admin). For billing-specific admin endpoints (Tier-3 plans, Stripe config status) see the `billing` category. Secret-setting paths go through redirect-URL tools (`get_stripe_config_url`, `get_plan_management_url`, `get_agent_secrets_url`).",
1209
+ endpoints: [
1210
+ {
1211
+ method: "GET",
1212
+ path: "/v1/organization-admin/{organization_id}/members",
1213
+ summary: "List active members of the Organization (admin view)",
1214
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1215
+ response: {
1216
+ items: "array of OrganizationMemberResponse: { user_id (uuid), email (string), display_name (string | null), avatar_url (string | null), role ('owner'|'admin'|'member'|'developer'), is_active (boolean), joined_at (ISO datetime) }",
1217
+ total: "number"
1218
+ },
1219
+ note: "Subset of /v1/organizations/{org_id}/members but enforces require_org_admin (rejects TenantMember JWT). Use this when building the Org Owner dashboard's Members page.",
1220
+ errors: {
1221
+ "403": "Caller is not an OrganizationMember with role owner/admin of the path's org."
1222
+ }
1223
+ },
1224
+ {
1225
+ method: "PATCH",
1226
+ path: "/v1/organization-admin/{organization_id}/members/{member_user_id}/role",
1227
+ summary: "Update a member's role within the Organization",
1228
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1229
+ request_body: {
1230
+ role: "string (required) — one of 'owner', 'admin', 'member', 'developer'."
1231
+ },
1232
+ response: "OrganizationMemberResponse — the updated member row.",
1233
+ notes: "Service-level guards: only owners can promote to owner; admins cannot touch owners; the last active owner cannot be demoted (returns 400).",
1234
+ errors: {
1235
+ "400": "Last-owner demotion guard / target not a member.",
1236
+ "403": "Insufficient role for the requested transition (e.g. admin trying to touch an owner).",
1237
+ "404": "Member not found in this organization."
1238
+ }
1239
+ },
1240
+ {
1241
+ method: "DELETE",
1242
+ path: "/v1/organization-admin/{organization_id}/members/{member_user_id}",
1243
+ summary: "Remove a member from the Organization",
1244
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1245
+ response: "204 No Content",
1246
+ notes: "Service-level guards: only owners can remove other owners; the last active owner cannot be removed.",
1247
+ errors: {
1248
+ "400": "Last-owner removal guard.",
1249
+ "403": "Insufficient role for removal (e.g. admin trying to remove an owner).",
1250
+ "404": "Member not found in this organization."
1251
+ }
1252
+ },
1253
+ {
1254
+ method: "GET",
1255
+ path: "/v1/organization-admin/{organization_id}/agents",
1256
+ summary: "List agents visible to the Organization (Hybrid Catalog — ADR-0006 v2)",
1257
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1258
+ query_params: {
1259
+ include_catalog: "boolean (default true) — when true, includes public agents from the Tenant's root Organization (the catalog). When false, returns only agents owned by this Organization.",
1260
+ skip: "integer (default 0, ≥ 0)",
1261
+ limit: "integer (default 100, 1 ≤ limit ≤ 200)"
1262
+ },
1263
+ response: {
1264
+ items: "array of AgentResponse: { id, tenant_id, organization_id, is_public, forked_from_agent_id, name, description, system_prompt, response_type, rag_provider, rag_role, is_active, rag_config, mcp_servers, created_by, created_at, updated_at }",
1265
+ total: "number"
1266
+ },
1267
+ note: "Visibility rules (ADR-0006 v2 Hybrid Catalog): agents where organization_id == this Org UNION (if include_catalog=true) agents where is_public=true AND organization_id is the Tenant's root Organization. To customize a catalog agent, the Org forks it via POST /v1/agents/{id}/fork (Tenant Dashboard endpoint — not exposed to end-users in the MCP).",
1268
+ errors: {
1269
+ "403": "Caller is not an OrganizationMember with role owner/admin of the path's org."
1270
+ }
1271
+ },
1272
+ // ---------------------------------------------------------------
1273
+ // Knowledge Base management (per-Org, ADR-0006 v2 §rag_role).
1274
+ // Org-scoped CRUD on KnowledgeBase + Document. Replaces the legacy
1275
+ // single-store rag_config.store_id path (closes cross-org RAG leak G5).
1276
+ // ---------------------------------------------------------------
1277
+ {
1278
+ method: "POST",
1279
+ path: "/v1/organization-admin/{organization_id}/knowledge-bases",
1280
+ summary: "Create a KnowledgeBase in the caller's Organization",
1281
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1282
+ request_body: {
1283
+ name: "string (optional, max 200)",
1284
+ description: "string (optional)",
1285
+ role_tag: "string (optional, max 100) — Declarative tag matched against Agent.rag_role at runtime. When an Agent is invoked, the runtime looks up a KB in the caller's Org with role_tag == agent.rag_role and uses its vector store.",
1286
+ vector_store_config: "object (optional) — provider-specific vector-store settings (e.g. OpenAI File Search store id).",
1287
+ embedding_config: "object (optional) — provider-specific embedding settings."
1288
+ },
1289
+ response: {
1290
+ id: "uuid",
1291
+ organization_id: "uuid",
1292
+ name: "string | null",
1293
+ description: "string | null",
1294
+ role_tag: "string | null",
1295
+ is_active: "boolean",
1296
+ is_processing: "boolean"
1297
+ },
1298
+ notes: "KBs are per-Organization (KnowledgeBase.organization_id REQUIRED). Agents bind to per-Org KBs via the `rag_role` tag — the Agent declares `rag_role='support'` and the runtime finds the calling Org's KB with `role_tag='support'`. Multiple Orgs can have a KB with the same `role_tag`; each Org sees only its own."
1299
+ },
1300
+ {
1301
+ method: "GET",
1302
+ path: "/v1/organization-admin/{organization_id}/knowledge-bases",
1303
+ summary: "List KnowledgeBases in the caller's Organization",
1304
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1305
+ query_params: {
1306
+ include_inactive: "boolean (default false)",
1307
+ skip: "integer (default 0, ≥ 0)",
1308
+ limit: "integer (default 100, 1 ≤ limit ≤ 200)"
1309
+ },
1310
+ response: "Array of KnowledgeBaseResponse (same shape as POST response)."
1311
+ },
1312
+ {
1313
+ method: "GET",
1314
+ path: "/v1/organization-admin/{organization_id}/knowledge-bases/{kb_id}",
1315
+ summary: "Get a KnowledgeBase by ID",
1316
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1317
+ response: "KnowledgeBaseResponse",
1318
+ errors: {
1319
+ "404": "KB not found OR kb.organization_id != path organization_id."
1320
+ }
1321
+ },
1322
+ {
1323
+ method: "PATCH",
1324
+ path: "/v1/organization-admin/{organization_id}/knowledge-bases/{kb_id}",
1325
+ summary: "Update a KnowledgeBase (partial)",
1326
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1327
+ request_body: {
1328
+ name: "string (optional)",
1329
+ description: "string (optional)",
1330
+ role_tag: "string (optional)",
1331
+ vector_store_config: "object (optional)",
1332
+ embedding_config: "object (optional)",
1333
+ is_active: "boolean (optional)"
1334
+ },
1335
+ response: "KnowledgeBaseResponse"
1336
+ },
1337
+ {
1338
+ method: "DELETE",
1339
+ path: "/v1/organization-admin/{organization_id}/knowledge-bases/{kb_id}",
1340
+ summary: "Soft delete a KnowledgeBase (is_active=false). Documents survive for audit.",
1341
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1342
+ response: "204 No Content"
1343
+ },
1344
+ {
1345
+ method: "POST",
1346
+ path: "/v1/organization-admin/{organization_id}/knowledge-bases/{kb_id}/documents",
1347
+ summary: "Upload a document to a KnowledgeBase",
1348
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1349
+ request_body: "multipart/form-data with `file` (single file). File size hard cap: 100MB (413 if exceeded).",
1350
+ response: {
1351
+ id: "integer",
1352
+ knowledge_base_id: "uuid",
1353
+ organization_id: "uuid",
1354
+ filename: "string",
1355
+ file_size: "integer (bytes)",
1356
+ content_type: "string",
1357
+ status: "string — UPLOADED initially; worker layer transitions to PROCESSING/READY/FAILED.",
1358
+ error_message: "string | null"
1359
+ },
1360
+ notes: "The controller only records metadata; the worker layer consumes Document rows with status=UPLOADED and pushes to the storage provider. Cross-Org uploads (kb.organization_id != caller's Org) return 404, never 403.",
1361
+ errors: {
1362
+ "400": "Empty file or missing filename.",
1363
+ "413": "File size exceeds 100MB."
1364
+ }
1365
+ },
1366
+ {
1367
+ method: "GET",
1368
+ path: "/v1/organization-admin/{organization_id}/knowledge-bases/{kb_id}/documents",
1369
+ summary: "List documents in a KnowledgeBase",
1370
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1371
+ query_params: {
1372
+ skip: "integer (default 0, ≥ 0)",
1373
+ limit: "integer (default 100, 1 ≤ limit ≤ 200)"
1374
+ },
1375
+ response: "Array of DocumentResponse."
1376
+ },
1377
+ {
1378
+ method: "GET",
1379
+ path: "/v1/organization-admin/{organization_id}/knowledge-bases/{kb_id}/documents/{document_id}",
1380
+ summary: "Get a document by ID",
1381
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1382
+ response: "DocumentResponse"
1383
+ },
1384
+ {
1385
+ method: "DELETE",
1386
+ path: "/v1/organization-admin/{organization_id}/knowledge-bases/{kb_id}/documents/{document_id}",
1387
+ summary: "Delete a document from a KnowledgeBase",
1388
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true, require_org_admin: true },
1389
+ response: "204 No Content"
1010
1390
  }
1011
1391
  ]
1012
1392
  },
@@ -1353,8 +1733,26 @@ const END_USER_ENDPOINTS = {
1353
1733
  ]
1354
1734
  },
1355
1735
  users: {
1356
- description: "User management - CRUD, settings, activation/deactivation. Dual auth: sk_live_* for admin ops, pk_live_* + JWT for self-service.",
1736
+ description: "User management - CRUD, settings, activation/deactivation, and Tenant-defined `profile_data` extensions (ADR-0010). Dual auth: sk_live_* for admin ops, pk_live_* + JWT for self-service.",
1357
1737
  endpoints: [
1738
+ {
1739
+ method: "PATCH",
1740
+ path: "/v1/users/me/profile-data",
1741
+ summary: "Merge keys into the authenticated user's profile_data (ADR-0010)",
1742
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1743
+ request_body: {
1744
+ profile_data: "object (required) — Top-level keys to merge into the existing profile_data JSONB. Keys present here overwrite same-named keys; keys absent are preserved. To clear a key, send it with value `null`. To clear the whole document, a future DELETE endpoint will be added (not implemented yet)."
1745
+ },
1746
+ response: "UserResponse — the updated row, including the merged profile_data. Same shape as GET /v1/users/{user_id}.",
1747
+ notes: "Merge semantics, NOT replace. The target user_id is taken from the decoded JWT — there is no body/path parameter that can redirect the write, so a user can ONLY edit their own profile_data. There is no admin-override path on this endpoint by design (use PUT /v1/users/{user_id} with sk_live_* if a Tenant admin needs to mutate someone else's row; that endpoint does NOT touch profile_data today). The platform does NOT validate against any schema — the Tenant owns the shape end-to-end. PII warning: do NOT store unencrypted PII (passwords, SSNs, card numbers); the column is plaintext at rest and ships on every /me call. Tenants needing per-Org-scoped or schema-validated user metadata can layer a custom entity (e.g. `user_profiles`) on top.",
1748
+ example_request: `{
1749
+ "profile_data": {
1750
+ "phone": "+1-555-0123",
1751
+ "preferences": { "theme": "dark", "lang": "en" },
1752
+ "premium_tier": "gold"
1753
+ }
1754
+ }`
1755
+ },
1358
1756
  {
1359
1757
  method: "POST",
1360
1758
  path: "/v1/users",
@@ -1384,6 +1782,7 @@ const END_USER_ENDPOINTS = {
1384
1782
  status: "string",
1385
1783
  current_plan_id: "uuid | null",
1386
1784
  default_organization_id: "uuid | null",
1785
+ profile_data: "object — Tenant-defined free-form metadata (ADR-0010). Defaults to {} when empty (POST does NOT seed this — mutate via PATCH /v1/users/me/profile-data afterward).",
1387
1786
  created_at: "ISO datetime",
1388
1787
  updated_at: "ISO datetime",
1389
1788
  settings: "UserSettingsResponse | null"
@@ -1441,6 +1840,7 @@ const END_USER_ENDPOINTS = {
1441
1840
  status: "string",
1442
1841
  current_plan_id: "uuid | null",
1443
1842
  default_organization_id: "uuid | null",
1843
+ profile_data: "object — Tenant-defined free-form metadata (ADR-0010). Defaults to {} when empty.",
1444
1844
  created_at: "ISO datetime",
1445
1845
  updated_at: "ISO datetime",
1446
1846
  settings: "UserSettingsResponse | null (only if include_settings=true)"
@@ -1581,7 +1981,7 @@ const END_USER_ENDPOINTS = {
1581
1981
  ]
1582
1982
  },
1583
1983
  entities: {
1584
- 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).",
1984
+ 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). Phase 3 additions: `reference` field type + cross-schema `join` clauses (ADR-0009), and reserved-slug enforcement (ADR-0011). Use `get_reserved_schema_slugs()` to pre-validate slugs.",
1585
1985
  endpoints: [
1586
1986
  // ── End-User Schema Endpoints ──
1587
1987
  {
@@ -1615,7 +2015,8 @@ const END_USER_ENDPOINTS = {
1615
2015
  enum: '{"type":"enum","required":true,"values":["draft","published","archived"]} // KEY IS "values", NOT "choices" — Genlobe rejects "choices" with 400.',
1616
2016
  email: '{"type":"email","required":true,"max_length":255}',
1617
2017
  url: '{"type":"url","required":false,"max_length":2048}',
1618
- phone: '{"type":"phone","required":false,"max_length":20}'
2018
+ phone: '{"type":"phone","required":false,"max_length":20}',
2019
+ reference: '{"type":"reference","target_schema_slug":"users","required":true,"on_delete":"set_null"} // ADR-0009. target_schema_slug must be lowercase letters/numbers/hyphens and reference an existing schema slug in the same Org — use the literal "users" for the native users table. on_delete is one of "set_null" (default), "restrict", or "cascade". Self-references (target_schema_slug == this schema\'s slug) are rejected with 400.'
1619
2020
  },
1620
2021
  example_request: `{
1621
2022
  "name": "Blog Post",
@@ -1625,10 +2026,11 @@ const END_USER_ENDPOINTS = {
1625
2026
  "title": {"type": "string", "required": true, "max_length": 255},
1626
2027
  "body": {"type": "text", "required": true},
1627
2028
  "status": {"type": "enum", "required": true, "values": ["draft", "published", "archived"]},
1628
- "views": {"type": "integer", "required": false, "min": 0}
2029
+ "views": {"type": "integer", "required": false, "min": 0},
2030
+ "author": {"type": "reference", "target_schema_slug": "users", "required": true, "on_delete": "restrict"}
1629
2031
  }
1630
2032
  }`,
1631
- notes: "Returns 409 if name or slug already exists. Common pitfall: the enum field uses 'values' (an array) — 'choices' is rejected with a 400 that names this directly. The validator runs at field level, so all errors come back together in `detail.errors[]`."
2033
+ notes: "Returns 409 if name or slug already exists. Returns 400 if the slug collides with a native DB table (e.g. `users`, `conversations`, `agents`, `subscriptions`, ...) — ADR-0011 reserves 53 such slugs (case-insensitive). Call `get_reserved_schema_slugs()` to get the full list and pre-validate before POSTing. Common pitfall: the enum field uses 'values' (an array) — 'choices' is rejected with a 400 that names this directly. Reference fields (ADR-0009) are validated app-side (no SQL FK from JSONB) — `target_schema_slug` MUST point at an existing schema in the same Organization, or the literal `users` for the native users table. The validator runs at field level, so all errors come back together in `detail.errors[]`."
1632
2034
  },
1633
2035
  {
1634
2036
  method: "GET",
@@ -1660,12 +2062,24 @@ const END_USER_ENDPOINTS = {
1660
2062
  method: "POST",
1661
2063
  path: "/v1/entity/records",
1662
2064
  summary: "Create a new entity record",
1663
- auth: { api_key: "sk_live_* or pk_live_*", jwt: false, headers: "X-Organization-Id required when key is not organization-scoped" },
2065
+ auth: { api_key: "sk_live_* or pk_live_*", jwt: "optional (required for pk_live_*)", headers: "X-Organization-Id required when key is not organization-scoped" },
1664
2066
  request_body: {
1665
2067
  schema_id: "uuid (required)",
1666
2068
  data: "object (required) - validated against schema fields_definition"
1667
2069
  },
1668
- notes: "Two modes: (1) sk_live_* without JWT -> tenant-scoped record, created_by_id is NULL (e.g. catalog/blog/global config). (2) API key + end-user JWT -> user-scoped record, created_by_id = user.id. The endpoint accepts both shapes; ownership is determined by whether a JWT is present."
2070
+ notes: "Three modes (#178 + #350): (1) sk_live_* alone -> tenant-scoped record, created_by_id is NULL (catalog/blog/global config). (2) sk_live_* + end-user JWT -> user-scoped record, created_by_id = user.id (server-side caller acting on behalf of a user). (3) pk_live_* + end-user JWT -> user-scoped record, created_by_id = user.id (canonical 'two-phase' frontend pattern: pk in the browser, JWT after login). pk_live_* WITHOUT a JWT is rejected with 401 — a public key alone is anonymous and can't be the creator of a record."
2071
+ },
2072
+ {
2073
+ method: "GET",
2074
+ path: "/v1/entity/records/mine",
2075
+ summary: "List records authored by the current end-user (\"my data\")",
2076
+ auth: { api_key: "sk_live_* or pk_live_*", jwt: true, headers: "X-Organization-Id required when key is not organization-scoped" },
2077
+ query_params: {
2078
+ schema_id: "uuid (required) - schema whose records to list",
2079
+ page: "number (default 1)",
2080
+ size: "number (default 20, max 100)"
2081
+ },
2082
+ notes: "Returns ONLY records where created_by_id = current_user.id, regardless of the user's role inside the Org (members and elevated roles alike). Natural path for end-user 'my data' queries (my chats, my orders, my notes). Use this instead of POST /v1/entity/records/search with manual created_by_id filters — the row-level scope (ADR-0007) is enforced server-side, frontend MUST NOT be the filter. Requires an end-user JWT; tenant-scoped (sk_live_* alone) callers get 401 — they should use POST /v1/entity/records/search."
1669
2083
  },
1670
2084
  {
1671
2085
  method: "GET",
@@ -1692,11 +2106,12 @@ const END_USER_ENDPOINTS = {
1692
2106
  {
1693
2107
  method: "POST",
1694
2108
  path: "/v1/entity/records/search",
1695
- summary: "Advanced search for entity records with filters and ordering",
2109
+ summary: "Advanced search for entity records with filters, ordering, and cross-schema joins (ADR-0009)",
1696
2110
  auth: { api_key: "sk_live_* or pk_live_*", jwt: false, headers: "X-Organization-Id required when key is not organization-scoped" },
1697
2111
  request_body: {
1698
2112
  schema_id: "uuid (optional) - filter by schema",
1699
- query: "object (optional) - filters: {field: value} for equality, {field: {operator: 'gt', value: 100}} for operators"
2113
+ query: "object (optional) - filters: {field: value} for equality, {field: {operator: 'gt', value: 100}} for operators",
2114
+ join: "array (optional, ADR-0009) — Cross-schema inner-join clauses against reference fields. Each item: { 'field': string (reference field on source schema), 'target_schema_slug': string (must match the field's declared target; use 'users' for the native users table), 'where': object (optional, JSONB equality predicates applied to the target's data; empty means 'just confirm the target exists') }. Multiple clauses are AND-ed. Inner-join only — no left/right. The target's fields are NOT returned to the caller; joins are filter-only. `where` does NOT support nested joins. Implemented as one EXISTS sub-select per clause (no N+1)."
1700
2115
  },
1701
2116
  query_params: {
1702
2117
  page: "number (default 1)",
@@ -1704,14 +2119,21 @@ const END_USER_ENDPOINTS = {
1704
2119
  order_by: "string (default 'created_at')",
1705
2120
  order_dir: "'asc' | 'desc' (default 'desc')"
1706
2121
  },
1707
- notes: "Supported operators: eq, ne, gt, gte, lt, lte, like, ilike, in, not_in",
2122
+ notes: "Supported operators in `query`: eq, ne, gt, gte, lt, lte, like, ilike, in, not_in. Row-level scope (ADR-0007): regular end-users (role=member) only see records where created_by_id = current_user.id; OrgAdmin and TenantMember bypass via include_all=true (audit-logged). Use the simpler GET /v1/entity/records/mine for the common 'my data' query.",
1708
2123
  example_request: `{
1709
2124
  "schema_id": "550e8400-e29b-41d4-a716-446655440000",
1710
2125
  "query": {
1711
2126
  "price": { "operator": "gt", "value": 100 },
1712
2127
  "category": "electronics",
1713
2128
  "status": { "operator": "in", "value": ["active", "pending"] }
1714
- }
2129
+ },
2130
+ "join": [
2131
+ {
2132
+ "field": "author",
2133
+ "target_schema_slug": "users",
2134
+ "where": { "is_active": true }
2135
+ }
2136
+ ]
1715
2137
  }`
1716
2138
  }
1717
2139
  ]
@@ -3091,25 +3513,105 @@ Call this first to understand the API architecture and authentication model.`,
3091
3513
  },
3092
3514
  {
3093
3515
  name: "get_end_user_endpoints",
3094
- description: `Get detailed documentation for END-USER API endpoints.
3516
+ description: `Get detailed documentation for END-USER API endpoints, filtered to ONE category.
3095
3517
 
3096
3518
  Use this when building a frontend for end-users. These endpoints use:
3097
3519
  - API Key (X-API-Key header) - required for ALL requests
3098
3520
  - JWT token (Authorization: Bearer) - required AFTER login
3099
3521
 
3100
- Categories: authentication, organizations, subscriptions, billing, usage, agents, users, plans, ai, entities, departments, external_agents`,
3522
+ PREFER passing a single category. The output is focused on that category + a
3523
+ one-line pointer to the others. Calling without a category dumps the entire
3524
+ catalog (every endpoint of every category) and is rarely what you want — it
3525
+ burns context quickly.
3526
+
3527
+ If you don't know which categories exist yet, call list_endpoint_categories()
3528
+ first.
3529
+
3530
+ Categories: authentication, organizations, subscriptions, billing (incl. Tier-3 admin read-only endpoints + subscriptions list), organization_admin (non-billing Org Owner Dashboard surfaces — members, customers, agents, KB, notifications, webhook audit), usage, agents, users (incl. PATCH /users/me/profile-data — ADR-0010), plans, ai, entities (Phase 3: reference fields + cross-schema join — ADR-0009; reserved slugs — ADR-0011), departments, files, external_agents.
3531
+
3532
+ The 'billing' category covers both the end-user surface
3533
+ (/v1/organizations/{org_id}/billing/*) and the Organization-admin read-only
3534
+ billing surface (/v1/organization-admin/{org_id}/{plans, config/stripe, audit, subscriptions}).
3535
+ Secret-setting paths (config/stripe POST/PATCH/DELETE, plan CRUD that needs
3536
+ the Stripe key) go through get_stripe_config_url / get_plan_management_url —
3537
+ the MCP never accepts raw secrets inline.
3538
+
3539
+ The 'organization_admin' category covers the non-billing admin surfaces:
3540
+ members CRUD, customers list, agents visibility (Hybrid Catalog — ADR-0006 v2),
3541
+ KnowledgeBase + Documents CRUD (per-Org via rag_role), agent-change
3542
+ notifications, and the Stripe webhook event audit log.`,
3101
3543
  inputSchema: {
3102
3544
  type: "object",
3103
3545
  properties: {
3104
3546
  category: {
3105
3547
  type: "string",
3106
- description: "Filter by category. Leave empty for all endpoints.",
3107
- enum: ["authentication", "organizations", "subscriptions", "billing", "usage", "agents", "users", "plans", "ai", "entities", "departments", "files", "external_agents"],
3548
+ description: "Filter by category (strongly recommended). Leave empty only when you explicitly want the full catalog.",
3549
+ enum: ["authentication", "organizations", "subscriptions", "billing", "organization_admin", "usage", "agents", "users", "plans", "ai", "entities", "departments", "files", "external_agents"],
3108
3550
  },
3109
3551
  },
3110
3552
  required: [],
3111
3553
  },
3112
3554
  },
3555
+ {
3556
+ name: "list_endpoint_categories",
3557
+ description: `Return the bare list of available end-user endpoint categories plus a one-line summary of each. Cheap call (~30 tokens). Use this once if you don't already know what's available; then call get_end_user_endpoints({ category }) for the actual endpoint docs of the category you care about. This replaces the old per-call "Quick Reference" dump that get_end_user_endpoints used to print at the bottom of every response.`,
3558
+ inputSchema: {
3559
+ type: "object",
3560
+ properties: {},
3561
+ required: [],
3562
+ },
3563
+ },
3564
+ {
3565
+ name: "get_stripe_config_url",
3566
+ description: `Return the Dashboard URL where the Organization Owner sets or rotates the Organization's Stripe credentials (secret_key, publishable_key, webhook_secret). The MCP NEVER accepts raw secrets through tool input — this redirect URL is the canonical way to set them. Pure URL computation; no network call. Pair it with GET /v1/organization-admin/{org_id}/config/stripe to detect whether the Organization has Stripe configured already.`,
3567
+ inputSchema: {
3568
+ type: "object",
3569
+ properties: {
3570
+ org_id: {
3571
+ type: "string",
3572
+ description: "The Organization id (UUID) whose Stripe config the owner wants to manage.",
3573
+ },
3574
+ },
3575
+ required: ["org_id"],
3576
+ },
3577
+ },
3578
+ {
3579
+ name: "get_agent_secrets_url",
3580
+ description: `Return the Dashboard URL where the Tenant rotates / sets an Agent's provider keys (OpenAI / Anthropic / etc.) and webhook secrets. The MCP NEVER accepts raw secrets through tool input — this redirect URL is the canonical way to set them. Pure URL computation; no network call.`,
3581
+ inputSchema: {
3582
+ type: "object",
3583
+ properties: {
3584
+ agent_id: {
3585
+ type: "string",
3586
+ description: "The Agent id (UUID) whose secrets the developer wants to manage.",
3587
+ },
3588
+ },
3589
+ required: ["agent_id"],
3590
+ },
3591
+ },
3592
+ {
3593
+ name: "get_plan_management_url",
3594
+ description: `Return the Dashboard URL where the Organization Owner creates / edits / deletes Plans (Tier-3, BYO-Stripe). Write operations are NOT exposed via MCP because plan sync needs the Organization's Stripe secret_key; the MCP only exposes the read paths (GET /v1/organization-admin/{org_id}/plans, GET /v1/organization-admin/{org_id}/plans/{plan_id}) for inspection. Pure URL computation; no network call.`,
3595
+ inputSchema: {
3596
+ type: "object",
3597
+ properties: {
3598
+ org_id: {
3599
+ type: "string",
3600
+ description: "The Organization id (UUID) whose plan catalog the owner wants to manage.",
3601
+ },
3602
+ },
3603
+ required: ["org_id"],
3604
+ },
3605
+ },
3606
+ {
3607
+ name: "get_reserved_schema_slugs",
3608
+ description: `Return the full list of schema slugs the Custom Entities subsystem reserves (ADR-0011). These slugs collide with native DB tables (e.g. \`users\`, \`conversations\`, \`agents\`, \`subscriptions\`, \`alembic_version\`, ...) and CANNOT be used as a custom data schema slug — the server rejects them at \`POST /v1/entity/schemas\` with a 400. Comparison is case-insensitive and ignores surrounding whitespace. Call this tool BEFORE issuing a \`POST /v1/entity/schemas\` so the agent can refuse client-side instead of round-tripping a 400. Pure data; no network call.`,
3609
+ inputSchema: {
3610
+ type: "object",
3611
+ properties: {},
3612
+ required: [],
3613
+ },
3614
+ },
3113
3615
  {
3114
3616
  name: "get_request_headers",
3115
3617
  description: "Get detailed information about required HTTP headers for API requests.",
@@ -3327,125 +3829,153 @@ Skipping these is how secret keys end up shipped to a browser bundle. Don't skip
3327
3829
  };
3328
3830
  }
3329
3831
  case "get_end_user_endpoints": {
3832
+ // Phase-1 P0 (#347): focused output. Return ONLY the requested category
3833
+ // plus a one-line pointer to the other categories. The previous
3834
+ // implementation dumped a global Quick Reference of every category
3835
+ // at the bottom of every per-category call — that burned thousands
3836
+ // of tokens of agent context for no benefit (see CHANGELOG 3.5.0).
3330
3837
  const category = args.category;
3331
- let endpoints = END_USER_ENDPOINTS;
3838
+ const allCategories = Object.keys(END_USER_ENDPOINTS);
3332
3839
  if (category && category in END_USER_ENDPOINTS) {
3333
- endpoints = { [category]: END_USER_ENDPOINTS[category] };
3840
+ const focused = { [category]: END_USER_ENDPOINTS[category] };
3841
+ const otherCategories = allCategories.filter((c) => c !== category);
3842
+ return {
3843
+ content: [
3844
+ {
3845
+ type: "text",
3846
+ text: `# End-User API Endpoints — ${category}
3847
+
3848
+ These endpoints are for building frontend applications for end-users.
3849
+
3850
+ ${JSON.stringify(focused, null, 2)}
3851
+
3852
+ ---
3853
+ For other categories, call get_end_user_endpoints again with category=${otherCategories.join(" | ")}.
3854
+ Use list_endpoint_categories() if you just want the bare list of available categories.`,
3855
+ },
3856
+ ],
3857
+ };
3334
3858
  }
3859
+ // No category filter → return everything. This is the explicit
3860
+ // "give me the whole catalog" path; the focused path above is
3861
+ // what the agent should use on every other call.
3335
3862
  return {
3336
3863
  content: [
3337
3864
  {
3338
3865
  type: "text",
3339
- text: `# End-User API Endpoints${category ? ` - ${category}` : ''}
3866
+ text: `# End-User API Endpoints ALL categories
3340
3867
 
3341
3868
  These endpoints are for building frontend applications for end-users.
3342
3869
 
3343
- ${JSON.stringify(endpoints, null, 2)}
3344
-
3345
- ## Quick Reference
3346
-
3347
- ### Authentication (No JWT required)
3348
- - POST /v1/auth/register - Register new user
3349
- - POST /v1/auth/login - Login user
3350
- - POST /v1/auth/refresh - Refresh access token
3351
- - POST /v1/auth/forgot-password - Request password reset
3352
- - POST /v1/auth/reset-password - Reset password
3353
- - POST /v1/auth/verify-email - Verify email (with API Key)
3354
- - POST /v1/auth/verify-email-with-token - Verify email with token only (no API Key)
3355
- - POST /v1/auth/resend-verification - Resend verification email
3356
- - GET /v1/auth/providers - List enabled OAuth providers (e.g. Google)
3357
- - GET /v1/auth/google/url?redirect_uri=... - Get Google OAuth URL
3358
- - GET /v1/auth/google/callback - OAuth callback (redirects with tokens in URL fragment #)
3359
-
3360
- ### Authenticated Endpoints (JWT required)
3361
- - GET /v1/auth/me - Get current user
3362
- - POST /v1/auth/logout - Logout
3363
- - POST /v1/auth/change-password - Change password (authenticated)
3364
- - GET /v1/organizations/my-organizations - Get user's organizations
3365
- - POST /v1/organizations - Create organization
3366
- - GET /v1/organizations/{id}/departments - List departments
3367
- - GET /v1/organizations/{id}/members - List members
3368
- - PUT /v1/organizations/{id}/members/{user_id} - Update member role
3369
- - DELETE /v1/organizations/{id}/members/{user_id} - Remove member
3370
- - POST /v1/organizations/{id}/invite - Invite member
3371
- - POST /v1/organizations/accept-invitation - Accept invitation
3372
- - POST /v1/organizations/reject-invitation - Reject invitation
3373
- - GET /v1/organizations/invitations/pending?email= - Check pending invitations
3374
- - GET /v1/organizations/invitations/{token} - Get invitation details
3375
- - POST /v1/organizations/{id}/set-default - Set default org
3376
- - GET /v1/organizations/{id}/subscription - Get org subscription
3377
- - POST /v1/organizations/{id}/subscription/{plan_id} - Subscribe to plan
3378
- - DELETE /v1/organizations/{id}/subscription - Cancel subscription
3379
- - GET /v1/organizations/{id}/usage - Get org usage
3380
-
3381
- ### Plans (JWT required)
3382
- - GET /v1/plans - List all plans (paginated)
3383
- - GET /v1/plans/active - List active plans
3384
- - GET /v1/plans/free - List free plans
3385
- - GET /v1/plans/{plan_id} - Get plan details
3386
-
3387
- ### Subscriptions (JWT required)
3388
- - GET /v1/subscriptions/plans - List plans
3389
- - GET /v1/subscriptions/plans/{plan_id} - Get plan by ID
3390
- - GET /v1/subscriptions/current - Get current subscription
3391
- - POST /v1/subscriptions/subscriptions - Create subscription
3392
- - GET /v1/subscriptions/subscriptions/{id} - Get subscription by ID
3393
- - PUT /v1/subscriptions/subscriptions/{id} - Update subscription
3394
- - DELETE /v1/subscriptions/subscriptions/{id} - Cancel subscription
3395
- - POST /v1/subscriptions/checkout - Start checkout
3396
- - GET /v1/subscriptions/users/{user_id}/usage - Get user plan usage
3397
- - GET /v1/subscriptions/users/{user_id}/usage/limits - Check user limits
3398
- - PUT /v1/subscriptions/users/{user_id}/plan/{plan_id} - Assign plan
3399
- - POST /v1/subscriptions/organizations/{org_id}/resume - Resume org subscription (sk_live_*)
3400
- - GET /v1/subscriptions/organizations/{org_id}/invoices - Get org invoices (sk_live_*)
3401
- - DELETE /v1/subscriptions/organizations/{org_id}/subscription - Cancel org subscription (sk_live_*)
3402
-
3403
- ### Billing (sk_live_* ONLY)
3404
- - GET /v1/billing/available-plans - List available plans
3405
- - POST /v1/billing/create-checkout-session - Create checkout session
3406
- - POST /v1/billing/change-plan - Change plan (upgrade/downgrade)
3407
- - POST /v1/billing/cancel-subscription - Cancel subscription
3408
- - GET /v1/billing/billing-history-org/{id} - Billing history
3409
-
3410
- ### Users
3411
- - POST /v1/users - Create user (sk_live_* + JWT)
3412
- - GET /v1/users - List users (sk_live_* only, paginated)
3413
- - GET /v1/users/stats - User stats (sk_live_* only)
3414
- - GET /v1/users/{id} - Get user (self or sk_live_*)
3415
- - PUT /v1/users/{id} - Update user
3416
- - PUT /v1/users/{id}/settings - Update settings
3417
- - POST /v1/users/{id}/deactivate - Deactivate user (soft delete)
3418
- - POST /v1/users/{id}/activate - Activate user (sk_live_* only)
3419
- - DELETE /v1/users/{id} - Delete user (sk_live_* only, soft delete)
3420
-
3421
- ### Usage (JWT required)
3422
- - GET /v1/api/usage/current - Current usage stats
3423
- - GET /v1/api/usage/history - Usage history (limit/offset pagination)
3424
- - GET /v1/api/usage/entity-usage - Entity usage stats
3425
- - GET /v1/usage/stats - Comprehensive usage statistics
3426
- - POST /v1/usage/check - Check if usage allowed (without consuming)
3427
- - POST /v1/usage/consume - Consume usage tokens/messages
3428
-
3429
- ### AI Agents (JWT required)
3430
- - GET /v1/user/agents/available - List available agents
3431
- - GET /v1/user/agents/{agent_id} - Get agent details
3432
- - POST /v1/user/agents/{agent_id}/chat - Chat with an agent
3433
- - GET /v1/user/agents/{agent_id}/history - Get conversation history
3434
-
3435
- ### Custom Entities (API Key + JWT + X-Organization-Id)
3436
- - GET /v1/entity/schemas - List schemas
3437
- - POST /v1/entity/schemas - Create schema
3438
- - GET /v1/entity/schemas/{id} - Get schema
3439
- - PUT /v1/entity/schemas/{id} - Update schema
3440
- - DELETE /v1/entity/schemas/{id} - Delete schema
3441
- - POST /v1/entity/records - Create record
3442
- - GET /v1/entity/records/{id} - Get record
3443
- - PUT /v1/entity/records/{id} - Update record
3444
- - DELETE /v1/entity/records/{id} - Delete record
3445
- - POST /v1/entity/records/search - Search records with filters
3446
-
3447
- ### Departments
3448
- - GET /v1/departments/{department_id} - Get department details`,
3870
+ ${JSON.stringify(END_USER_ENDPOINTS, null, 2)}
3871
+
3872
+ ---
3873
+ Tip: subsequent calls should pass a single category (one of: ${allCategories.join(", ")}) so the output stays focused on what the agent is working on.`,
3874
+ },
3875
+ ],
3876
+ };
3877
+ }
3878
+ case "list_endpoint_categories": {
3879
+ // Lightweight helper (#347): give the agent just the category names
3880
+ // so it can target subsequent get_end_user_endpoints calls without
3881
+ // having to first download the whole catalog. ~30 tokens.
3882
+ const categories = Object.keys(END_USER_ENDPOINTS);
3883
+ const summaries = {};
3884
+ for (const c of categories) {
3885
+ const data = END_USER_ENDPOINTS[c];
3886
+ summaries[c] = (data && typeof data.description === "string")
3887
+ ? data.description.split("\n")[0]
3888
+ : "";
3889
+ }
3890
+ return {
3891
+ content: [
3892
+ {
3893
+ type: "text",
3894
+ text: `# End-User Endpoint Categories
3895
+
3896
+ ${categories.length} categories available. Call get_end_user_endpoints({ category }) with one of them.
3897
+
3898
+ ${JSON.stringify({ categories, summaries }, null, 2)}`,
3899
+ },
3900
+ ],
3901
+ };
3902
+ }
3903
+ case "get_stripe_config_url": {
3904
+ const orgId = args.org_id;
3905
+ if (!orgId) {
3906
+ throw new Error("get_stripe_config_url requires org_id");
3907
+ }
3908
+ const url = `${FRONTEND_URL}/org-admin/${encodeURIComponent(orgId)}/integrations/stripe`;
3909
+ return {
3910
+ content: [
3911
+ {
3912
+ type: "text",
3913
+ text: JSON.stringify({
3914
+ url,
3915
+ reason: "Stripe secrets (secret_key, webhook_secret) must be set via the Organization Owner Dashboard, not inline through the MCP. The human developer pastes the keys; the MCP NEVER accepts or returns secret material.",
3916
+ next_action: { type: "open_in_browser", url },
3917
+ }, null, 2),
3918
+ },
3919
+ ],
3920
+ };
3921
+ }
3922
+ case "get_agent_secrets_url": {
3923
+ const agentId = args.agent_id;
3924
+ if (!agentId) {
3925
+ throw new Error("get_agent_secrets_url requires agent_id");
3926
+ }
3927
+ const url = `${FRONTEND_URL}/dashboard/agents/${encodeURIComponent(agentId)}/secrets`;
3928
+ return {
3929
+ content: [
3930
+ {
3931
+ type: "text",
3932
+ text: JSON.stringify({
3933
+ url,
3934
+ reason: "Agent provider keys (OpenAI / Anthropic / etc.) and webhook secrets must be set or rotated via the Tenant Dashboard, not inline through the MCP. The human developer pastes the keys; the MCP NEVER accepts or returns secret material.",
3935
+ next_action: { type: "open_in_browser", url },
3936
+ }, null, 2),
3937
+ },
3938
+ ],
3939
+ };
3940
+ }
3941
+ case "get_plan_management_url": {
3942
+ const orgId = args.org_id;
3943
+ if (!orgId) {
3944
+ throw new Error("get_plan_management_url requires org_id");
3945
+ }
3946
+ const url = `${FRONTEND_URL}/org-admin/${encodeURIComponent(orgId)}/plans`;
3947
+ return {
3948
+ content: [
3949
+ {
3950
+ type: "text",
3951
+ text: JSON.stringify({
3952
+ url,
3953
+ reason: "Plan create / update / delete (and the Stripe-product sync they trigger) needs the Organization's Stripe secret_key, which lives encrypted in OrganizationConfig and is never exposed to the agent. Use the Dashboard for write operations; the MCP exposes the read paths (GET /v1/organization-admin/{org_id}/plans, GET /v1/organization-admin/{org_id}/plans/{plan_id}) for inspection.",
3954
+ next_action: { type: "open_in_browser", url },
3955
+ }, null, 2),
3956
+ },
3957
+ ],
3958
+ };
3959
+ }
3960
+ case "get_reserved_schema_slugs": {
3961
+ // ADR-0011: returns the full list of slugs that cannot be used
3962
+ // for a Custom Entity schema because they collide with a native
3963
+ // DB table. Agents should call this BEFORE issuing
3964
+ // POST /v1/entity/schemas so they can refuse client-side.
3965
+ const slugs = [...RESERVED_SCHEMA_SLUGS];
3966
+ return {
3967
+ content: [
3968
+ {
3969
+ type: "text",
3970
+ text: JSON.stringify({
3971
+ reserved_slugs: slugs,
3972
+ total: slugs.length,
3973
+ comparison: "case-insensitive (server normalizes via .strip().lower() before comparison)",
3974
+ rejection_status: 400,
3975
+ rejection_message_template: "Schema slug '<slug>' is reserved (collides with a native table). Choose a different name.",
3976
+ source_of_truth: "src/tenants/services/custom_data_schema_service.py — RESERVED_SCHEMA_SLUGS. This MCP tool MUST be kept in sync; when a new native __tablename__ ships, both lists are updated in the same PR.",
3977
+ adr: "docs/adr/0011-namespace-collision-policy.md",
3978
+ }, null, 2),
3449
3979
  },
3450
3980
  ],
3451
3981
  };
@@ -4319,6 +4849,7 @@ type to pick.`,
4319
4849
  async function main() {
4320
4850
  console.error(`🚀 Multi-tenant SaaS API MCP Server v${SERVER_VERSION}`);
4321
4851
  console.error(`📡 API URL: ${API_URL}`);
4852
+ console.error(`🖥 Frontend URL (for redirect-URL tools): ${FRONTEND_URL}`);
4322
4853
  console.error(`🔑 API Key: ${API_KEY ? "Configured" : "Not configured (set SAAS_API_KEY to enable authenticated calls)"}`);
4323
4854
  console.error(`\n👉 Recommended first calls (vibecoding flow):`);
4324
4855
  console.error(` 1. validate_credentials — verify key + detect type (pk_live_* vs sk_live_*)`);
@@ -4326,13 +4857,20 @@ async function main() {
4326
4857
  console.error(` 3. recommend_stack — pick a framework that won't leak your key`);
4327
4858
  console.error(`\nReference tools:`);
4328
4859
  console.error(` - get_api_overview: Understand the API architecture`);
4329
- console.error(` - get_end_user_endpoints: Get endpoint documentation`);
4860
+ console.error(` - list_endpoint_categories: List endpoint categories (cheap)`);
4861
+ console.error(` - get_end_user_endpoints: Get endpoint documentation (one category at a time)`);
4330
4862
  console.error(` - get_sdk_template: Get TypeScript SDK template`);
4331
4863
  console.error(` - get_authentication_flow: Auth implementation guide`);
4332
4864
  console.error(` - get_common_patterns: Best practices and patterns`);
4333
4865
  console.error(` - search_endpoints: Search for endpoints`);
4334
4866
  console.error(` - get_endpoint_details: Get specific endpoint info`);
4335
4867
  console.error(` - get_openapi_spec: Get full OpenAPI spec`);
4868
+ console.error(`\nRedirect-URL tools (no secrets ever returned by the MCP):`);
4869
+ console.error(` - get_stripe_config_url — Dashboard URL to set/rotate the Org's Stripe keys`);
4870
+ console.error(` - get_agent_secrets_url — Dashboard URL to rotate an Agent's secrets`);
4871
+ console.error(` - get_plan_management_url — Dashboard URL to create/edit Plans`);
4872
+ console.error(`\nCustom Entities helpers (ADR-0011):`);
4873
+ console.error(` - get_reserved_schema_slugs — Pre-validate schema slug before POST /v1/entity/schemas`);
4336
4874
  console.error(`\nAuthenticated helpers:`);
4337
4875
  console.error(` - list_end_users (sk_live_* required)`);
4338
4876
  console.error(` - list_oauth_providers (any key)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genlobe/mcp-server",
3
- "version": "3.4.0",
3
+ "version": "3.6.1",
4
4
  "description": "MCP Server for GenLobe SaaS API - Provides AI assistants with comprehensive API documentation for building frontend applications",
5
5
  "main": "dist/index.js",
6
6
  "bin": {