@layers/amba-mcp 4.0.2 → 4.0.3

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 CHANGED
@@ -286,6 +286,234 @@ function warnDeprecatedAlias(alias, canonical) {
286
286
  console.warn(`[amba-mcp] tool "${alias}" is deprecated; use "${canonical}" instead. The alias still works but will be removed in a future release.`);
287
287
  }
288
288
  //#endregion
289
+ //#region src/lib/annotations.ts
290
+ /**
291
+ * Read-only verbs. A tool whose name contains one of these (and no
292
+ * destructive verb) does not mutate project state — it reads/derives/
293
+ * exports data. Maps to `readOnlyHint: true`.
294
+ */
295
+ const READ_VERBS = new Set([
296
+ "get",
297
+ "list",
298
+ "find",
299
+ "describe",
300
+ "stats",
301
+ "export",
302
+ "analytics",
303
+ "me",
304
+ "status",
305
+ "count",
306
+ "search",
307
+ "check",
308
+ "tiers",
309
+ "verify",
310
+ "logs",
311
+ "query"
312
+ ]);
313
+ /**
314
+ * Destructive verbs. A tool whose name contains one of these MAY perform
315
+ * a destructive update (irreversible removal of existing state). Maps to
316
+ * `destructiveHint: true`. `reset` is included because `*_reset_sandbox`
317
+ * wipes end-user data.
318
+ */
319
+ const DESTRUCTIVE_VERBS = new Set([
320
+ "delete",
321
+ "remove",
322
+ "revoke",
323
+ "drop",
324
+ "reset"
325
+ ]);
326
+ /**
327
+ * Write verbs — mutations that are additive / non-destructive (create a
328
+ * row, update fields, grant currency, deploy a function, send a push, …).
329
+ * Maps to `destructiveHint: false`. This set exists so a write tool that
330
+ * matches NONE of the read/destructive verbs is still positively
331
+ * classified (rather than falling through unannotated). Kept explicit so
332
+ * an unrecognised verb surfaces as a test failure instead of silently
333
+ * defaulting.
334
+ */
335
+ const WRITE_VERBS = new Set([
336
+ "create",
337
+ "update",
338
+ "set",
339
+ "patch",
340
+ "grant",
341
+ "spend",
342
+ "deduct",
343
+ "deploy",
344
+ "send",
345
+ "add",
346
+ "configure",
347
+ "invoke",
348
+ "schedule",
349
+ "assign",
350
+ "upload",
351
+ "rotate",
352
+ "reprovision",
353
+ "transfer",
354
+ "pause",
355
+ "resume",
356
+ "trigger",
357
+ "evaluate",
358
+ "test",
359
+ "approve",
360
+ "reject",
361
+ "escalate",
362
+ "replay",
363
+ "import",
364
+ "alter",
365
+ "insert",
366
+ "invite",
367
+ "register",
368
+ "purchase",
369
+ "signup",
370
+ "login",
371
+ "refresh"
372
+ ]);
373
+ /**
374
+ * Split a tool name into lowercase verb-candidate tokens, dropping the
375
+ * `amba_` prefix. Exported for the test suite.
376
+ */
377
+ function toolNameTokens(name) {
378
+ return name.replace(/^amba_/, "").split("_").filter(Boolean);
379
+ }
380
+ /**
381
+ * Classify a tool name into its behavioural class by PRECEDENCE
382
+ * (destructive > read > write). Returns `null` if no token matches any
383
+ * known verb — that should never happen for a real Amba tool and the
384
+ * test suite asserts it doesn't, so a `null` at runtime means a new tool
385
+ * used an unrecognised verb and needs a verb-set entry here.
386
+ */
387
+ function classifyToolVerb(name) {
388
+ const tokens = toolNameTokens(name);
389
+ let sawRead = false;
390
+ let sawWrite = false;
391
+ for (const token of tokens) {
392
+ if (DESTRUCTIVE_VERBS.has(token)) return "destructive";
393
+ if (READ_VERBS.has(token)) sawRead = true;
394
+ else if (WRITE_VERBS.has(token)) sawWrite = true;
395
+ }
396
+ if (sawRead) return "read";
397
+ if (sawWrite) return "write";
398
+ return null;
399
+ }
400
+ /** Title-case a single token (`currencies` → `Currencies`). */
401
+ function capitalize(token) {
402
+ return token.length === 0 ? token : token[0].toUpperCase() + token.slice(1);
403
+ }
404
+ /**
405
+ * Verb-set tokens that read as nouns/objects far more often than as the
406
+ * operation verb, so they must NOT be chosen as the title's leading verb.
407
+ * They stay in the verb sets above for CLASSIFICATION (e.g. `*_status`,
408
+ * `*_stats` are read-only), but for TITLE purposes they're objects:
409
+ *
410
+ * amba_get_friendship_stats → "Get Friendship Stats" (not "Stats …")
411
+ * amba_pause_function_schedule→ "Pause Function Schedule" (not "Schedule …")
412
+ * amba_functions_get_logs → "Get Functions Logs"
413
+ * amba_billing_status → "Billing Status" (no action verb at all)
414
+ * amba_sessions_analytics → "Sessions Analytics"
415
+ */
416
+ const NOUN_LIKE_VERBS = new Set([
417
+ "stats",
418
+ "status",
419
+ "logs",
420
+ "tiers",
421
+ "analytics",
422
+ "schedule",
423
+ "me"
424
+ ]);
425
+ /**
426
+ * The verbs eligible to be a title's leading word: every classification
427
+ * verb that isn't a noun-like token. These are unambiguous action verbs
428
+ * (`create`, `get`, `delete`, `set`, `pause`, …).
429
+ */
430
+ const STRONG_TITLE_VERBS = new Set([
431
+ ...READ_VERBS,
432
+ ...WRITE_VERBS,
433
+ ...DESTRUCTIVE_VERBS
434
+ ].filter((v) => !NOUN_LIKE_VERBS.has(v)));
435
+ /**
436
+ * Pick the index of the OPERATIVE action verb for the title, or -1 if the
437
+ * name has no action verb (e.g. `billing_status`, `sessions_analytics`).
438
+ *
439
+ * A naive `tokens.find()` is wrong two different ways:
440
+ * - `amba_currency_grant_rules_create` has `grant` (a noun-modifier in
441
+ * `grant_rules`) AND `create`; `find` picks `grant` → "Grant … Create".
442
+ * - `amba_get_friendship_stats` has `get` AND `stats`; preferring the
443
+ * trailing match picks `stats` → "Stats Get Friendship".
444
+ *
445
+ * Amba's naming puts the operation verb at a token-list BOUNDARY: leading
446
+ * for legacy `amba_<verb>_<resource>`, trailing/mid for canonical
447
+ * `amba_<resource>_<verb>[_<object>]`. So:
448
+ * 1. If the FIRST token is a strong verb → it's the operative verb
449
+ * (legacy alias: `get_friendship_stats`, `create_currency`).
450
+ * 2. Else take the LAST strong verb — skips trailing object nouns like
451
+ * `stats`/`schedule` (excluded from `STRONG_TITLE_VERBS`) and skips
452
+ * mid-name noun-modifiers like `grant` in `grant_rules_create`
453
+ * (`get_stats`/`set_price`/`grant_rules_create` all resolve right).
454
+ * 3. No strong verb → -1 (caller title-cases in order).
455
+ */
456
+ function operativeVerbIndex(tokens) {
457
+ if (tokens.length > 0 && STRONG_TITLE_VERBS.has(tokens[0])) return 0;
458
+ let lastStrong = -1;
459
+ for (let i = 0; i < tokens.length; i += 1) if (STRONG_TITLE_VERBS.has(tokens[i])) lastStrong = i;
460
+ return lastStrong;
461
+ }
462
+ /**
463
+ * Derive a human-readable Title-Case title from the tool name. The
464
+ * operative action verb (see `operativeVerbIndex`) is moved to the front
465
+ * so the title reads as an imperative; remaining tokens keep their order:
466
+ *
467
+ * amba_currencies_create → "Create Currencies"
468
+ * amba_content_delete_schedule → "Delete Content Schedule"
469
+ * amba_currency_grant_rules_create → "Create Currency Grant Rules"
470
+ * amba_get_friendship_stats → "Get Friendship Stats"
471
+ * amba_pause_function_schedule → "Pause Function Schedule"
472
+ * amba_catalog_items_set_price → "Set Catalog Items Price"
473
+ * amba_users_get → "Get Users"
474
+ *
475
+ * When there is no action verb (`billing_status`, `sessions_analytics`)
476
+ * the tokens are title-cased in order — a readable noun-phrase fallback.
477
+ */
478
+ function deriveToolTitle(name) {
479
+ const tokens = toolNameTokens(name);
480
+ const idx = operativeVerbIndex(tokens);
481
+ if (idx >= 0) {
482
+ const rest = tokens.filter((_, i) => i !== idx);
483
+ return [tokens[idx], ...rest].map(capitalize).join(" ");
484
+ }
485
+ return tokens.map(capitalize).join(" ");
486
+ }
487
+ /**
488
+ * Derive the full `ToolAnnotations` (title + read-only/destructive hints)
489
+ * for a tool name. This is the single function `with-pat.ts` calls for
490
+ * every tool it registers, so annotations stay uniform and automatic.
491
+ */
492
+ function deriveToolAnnotations(name) {
493
+ const title = deriveToolTitle(name);
494
+ switch (classifyToolVerb(name)) {
495
+ case "read": return {
496
+ title,
497
+ readOnlyHint: true
498
+ };
499
+ case "destructive": return {
500
+ title,
501
+ readOnlyHint: false,
502
+ destructiveHint: true
503
+ };
504
+ case "write": return {
505
+ title,
506
+ readOnlyHint: false,
507
+ destructiveHint: false
508
+ };
509
+ default: return {
510
+ title,
511
+ readOnlyHint: false,
512
+ destructiveHint: true
513
+ };
514
+ }
515
+ }
516
+ //#endregion
289
517
  //#region src/lib/with-pat.ts
290
518
  /**
291
519
  * The `pat` schema entry injected into every tool that goes through
@@ -362,8 +590,8 @@ function registerTool(server, apiClient, name, description, schema, handler, ali
362
590
  client
363
591
  });
364
592
  };
365
- server.tool(name, description, extendedSchema, wrappedHandler(name));
366
- for (const alias of aliases) server.tool(alias, description, extendedSchema, (async (rawArgs) => {
593
+ server.tool(name, description, extendedSchema, deriveToolAnnotations(name), wrappedHandler(name));
594
+ for (const alias of aliases) server.tool(alias, description, extendedSchema, deriveToolAnnotations(alias), (async (rawArgs) => {
367
595
  warnDeprecatedAlias(alias, name);
368
596
  return wrappedHandler(name)(rawArgs);
369
597
  }));
@@ -375,15 +603,15 @@ function registerTool(server, apiClient, name, description, schema, handler, ali
375
603
  * should go through `registerTool`.
376
604
  */
377
605
  function registerPublicTool(server, name, description, schema, handler, aliases = []) {
378
- server.tool(name, description, schema, handler);
379
- for (const alias of aliases) server.tool(alias, description, schema, (async (args) => {
606
+ server.tool(name, description, schema, deriveToolAnnotations(name), handler);
607
+ for (const alias of aliases) server.tool(alias, description, schema, deriveToolAnnotations(alias), (async (args) => {
380
608
  warnDeprecatedAlias(alias, name);
381
609
  return handler(args);
382
610
  }));
383
611
  }
384
612
  //#endregion
385
613
  //#region src/tools/projects.ts
386
- function registerTools$25(server, apiClient) {
614
+ function registerTools$29(server, apiClient) {
387
615
  registerTool(server, apiClient, "amba_projects_list", "List all Amba projects owned by the authenticated developer. Returns project id, name, bundle_id, platform, and environment.", {}, async (_, { client }) => {
388
616
  const result = await client.get("/projects");
389
617
  return { content: [{
@@ -490,7 +718,7 @@ function registerTools$25(server, apiClient) {
490
718
  }
491
719
  //#endregion
492
720
  //#region src/tools/push.ts
493
- function registerTools$24(server, apiClient) {
721
+ function registerTools$28(server, apiClient) {
494
722
  registerTool(server, apiClient, "amba_push_campaigns_create", "Create a new push notification campaign for a project. The campaign starts in \"draft\" status. You can optionally target a segment and schedule delivery.", {
495
723
  project_id: z.string().describe("The project ID"),
496
724
  title: z.string().describe("Push notification title shown to the user"),
@@ -620,7 +848,7 @@ const segmentRulesSchema = z.object({
620
848
  operator: z.enum(["AND", "OR"]).describe("Logical operator combining conditions"),
621
849
  conditions: z.array(segmentConditionSchema).describe("Array of filter conditions")
622
850
  });
623
- function registerTools$23(server, apiClient) {
851
+ function registerTools$27(server, apiClient) {
624
852
  registerTool(server, apiClient, "amba_segments_list", "List user segments for a project. Segments define groups of users based on rules (e.g. \"active in last 7 days\", \"premium users\"). Returns only project-custom segments by default. Pass include_system: true to also include the built-in system segments (e.g. \"all\", \"active_7d\", \"premium\").", {
625
853
  project_id: z.string().describe("The project ID"),
626
854
  include_system: z.boolean().optional().describe("Include built-in system segments in the result. Defaults to false.")
@@ -705,8 +933,8 @@ const configConditionSchema = z.object({
705
933
  percentage: z.number().optional().describe("Percentage rollout (0-100)"),
706
934
  value: z.unknown().describe("Override value for this condition")
707
935
  });
708
- function registerTools$22(server, apiClient) {
709
- registerTool(server, apiClient, "amba_configs_list", "List remote-config keys for a project. Returns key, value, value_type, description, conditions, and version per active row. Remote config lets you change app behavior without deploying an update. Returns only project-created config keys by default. Pass include_system: true to also include platform-provided defaults (e.g. app_version_min, maintenance_mode).", {
936
+ function registerTools$26(server, apiClient) {
937
+ registerTool(server, apiClient, "amba_remote_configs_list", "List remote-config keys for a project. Returns key, value, value_type, description, conditions, and version per active row. Remote config lets you change app behavior without deploying an update. Returns only project-created config keys by default. Pass include_system: true to also include platform-provided defaults (e.g. app_version_min, maintenance_mode).", {
710
938
  project_id: z.string().describe("The project ID"),
711
939
  include_system: z.boolean().optional().describe("Include platform-provided default config keys in the result. Defaults to false.")
712
940
  }, async ({ project_id, include_system }, { client }) => {
@@ -717,8 +945,8 @@ function registerTools$22(server, apiClient) {
717
945
  type: "text",
718
946
  text: JSON.stringify(result, null, 2)
719
947
  }] };
720
- }, ["amba_list_configs"]);
721
- registerTool(server, apiClient, "amba_configs_create", "Create or upsert a remote-config key. Supports string, number, boolean, and JSON value types. Can include segment-based or percentage-based conditions for targeted rollouts. The handler is upsert-on-key — calling this for an existing key replaces it.", {
948
+ }, ["amba_configs_list", "amba_list_configs"]);
949
+ registerTool(server, apiClient, "amba_remote_configs_create", "Create or upsert a remote-config key. Supports string, number, boolean, and JSON value types. Can include segment-based or percentage-based conditions for targeted rollouts. The handler is upsert-on-key — calling this for an existing key replaces it.", {
722
950
  project_id: z.string().describe("The project ID"),
723
951
  key: z.string().describe("Config key name (e.g. \"feature_paywall_v2\", \"daily_limit\")"),
724
952
  value: z.unknown().describe("The default config value"),
@@ -743,8 +971,8 @@ function registerTools$22(server, apiClient) {
743
971
  type: "text",
744
972
  text: JSON.stringify(result, null, 2)
745
973
  }] };
746
- }, ["amba_create_config"]);
747
- registerTool(server, apiClient, "amba_configs_update", "Partially update a single remote-config key. Only fields provided are touched — other fields are preserved. Updating `value` bumps the per-row version counter; any successful mutation also recomputes the singleton config_versions hash so cached clients see the change.", {
974
+ }, ["amba_configs_create", "amba_create_config"]);
975
+ registerTool(server, apiClient, "amba_remote_configs_update", "Partially update a single remote-config key. Only fields provided are touched — other fields are preserved. Updating `value` bumps the per-row version counter; any successful mutation also recomputes the singleton config_versions hash so cached clients see the change.", {
748
976
  project_id: z.string().describe("The project ID"),
749
977
  key: z.string().describe("The config key to update"),
750
978
  value: z.unknown().optional().describe("New default value (omit to leave unchanged)"),
@@ -760,8 +988,8 @@ function registerTools$22(server, apiClient) {
760
988
  type: "text",
761
989
  text: JSON.stringify(result, null, 2)
762
990
  }] };
763
- }, ["amba_update_config"]);
764
- registerTool(server, apiClient, "amba_configs_delete", "Delete a remote-config key. Idempotent — deleting a non-existent key returns success. Recomputes the config_versions hash so clients refresh cached config on next fetch.", {
991
+ }, ["amba_configs_update", "amba_update_config"]);
992
+ registerTool(server, apiClient, "amba_remote_configs_delete", "Delete a remote-config key. Idempotent — deleting a non-existent key returns success. Recomputes the config_versions hash so clients refresh cached config on next fetch.", {
765
993
  project_id: z.string().describe("The project ID"),
766
994
  key: z.string().describe("The config key to delete")
767
995
  }, async ({ project_id, key }, { client }) => {
@@ -770,7 +998,7 @@ function registerTools$22(server, apiClient) {
770
998
  type: "text",
771
999
  text: JSON.stringify(result, null, 2)
772
1000
  }] };
773
- }, ["amba_delete_config"]);
1001
+ }, ["amba_configs_delete", "amba_delete_config"]);
774
1002
  }
775
1003
  //#endregion
776
1004
  //#region src/tools/content.ts
@@ -783,7 +1011,7 @@ const contentItemSchema = z.object({
783
1011
  metadata: z.record(z.unknown()).optional().describe("Arbitrary metadata key-value pairs"),
784
1012
  is_premium: z.boolean().optional().describe("Whether this content requires a premium entitlement")
785
1013
  });
786
- function registerTools$21(server, apiClient) {
1014
+ function registerTools$25(server, apiClient) {
787
1015
  registerTool(server, apiClient, "amba_content_libraries_create", "Create a content library for a project. A content library is a collection of content items (tips, quotes, articles) that can be delivered on a schedule to keep users engaged.", {
788
1016
  project_id: z.string().describe("The project ID"),
789
1017
  name: z.string().describe("Library name (e.g. \"Daily Motivation\", \"Workout Tips\")"),
@@ -947,7 +1175,7 @@ function registerTools$21(server, apiClient) {
947
1175
  }
948
1176
  //#endregion
949
1177
  //#region src/tools/streaks.ts
950
- function registerTools$20(server, apiClient) {
1178
+ function registerTools$24(server, apiClient) {
951
1179
  registerTool(server, apiClient, "amba_streaks_create", "Create a streak definition for a project. Streaks track consecutive user engagement (e.g. daily logins, workout completions). Supports configurable periods, grace periods, and freeze mechanics.", {
952
1180
  project_id: z.string().describe("The project ID"),
953
1181
  key: z.string().regex(/^[a-z0-9_-]{1,64}$/).optional().describe("Stable identifier used by the SDK to qualify a streak without knowing its UUID: `Amba.streaks.qualify(\"daily_login\")`. Lowercase letters, digits, underscore, hyphen, 1-64 characters. Immutable after creation — changing it breaks live SDK calls. When omitted the server derives one from `name`."),
@@ -1021,11 +1249,25 @@ function registerTools$20(server, apiClient) {
1021
1249
  text: JSON.stringify(result, null, 2)
1022
1250
  }] };
1023
1251
  });
1252
+ registerTool(server, apiClient, "amba_streaks_reset", "Reset or correct a specific user's streak — for support fixing a wrongly-broken streak (e.g. after an outage). Sets the user's current count and marks the streak active again. Omit set_count to zero the streak (clean restart); pass set_count to restore it to a specific value (longest_count is bumped if exceeded). Returns 404 if the user has no streak row for this definition yet.", {
1253
+ project_id: z.string().describe("The project ID"),
1254
+ streak_id: z.string().describe("The streak definition ID"),
1255
+ app_user_id: z.string().describe("The user whose streak to reset"),
1256
+ set_count: z.number().int().nonnegative().optional().describe("Restore the streak to this count. Omit to zero the streak (clean restart).")
1257
+ }, async ({ project_id, streak_id, app_user_id, set_count }, { client }) => {
1258
+ const payload = { app_user_id };
1259
+ if (set_count !== void 0) payload.set_count = set_count;
1260
+ const result = await client.post(`/projects/${project_id}/streaks/${streak_id}/reset`, payload);
1261
+ return { content: [{
1262
+ type: "text",
1263
+ text: JSON.stringify(result, null, 2)
1264
+ }] };
1265
+ });
1024
1266
  }
1025
1267
  //#endregion
1026
1268
  //#region src/tools/integrations.ts
1027
1269
  const providerEnum = z.enum(INTEGRATION_PROVIDERS);
1028
- function registerTools$19(server, apiClient) {
1270
+ function registerTools$23(server, apiClient) {
1029
1271
  registerTool(server, apiClient, "amba_integrations_configure", "Configure (create or replace) a third-party integration for a project. Supported providers: \"apns\" (Apple Push), \"fcm\" (Firebase Cloud Messaging), \"revenuecat\" (subscription management), \"superwall\" (paywall management). Each provider requires specific config fields.", {
1030
1272
  project_id: z.string().describe("The project ID"),
1031
1273
  provider: providerEnum.describe("Integration provider name"),
@@ -1095,7 +1337,7 @@ const PERIOD_DAYS = {
1095
1337
  "30d": "30",
1096
1338
  "90d": "90"
1097
1339
  };
1098
- function registerTools$18(server, apiClient) {
1340
+ function registerTools$22(server, apiClient) {
1099
1341
  registerTool(server, apiClient, "amba_analytics_get", "Get daily analytics for a project. Returns per-day event counts and unique-user counts for the requested window, plus totals. Use this to verify that end-to-end event tracking is working after seeding and testing.", {
1100
1342
  project_id: z.string().describe("The project ID"),
1101
1343
  period: z.enum([
@@ -1118,7 +1360,7 @@ function registerTools$18(server, apiClient) {
1118
1360
  }
1119
1361
  //#endregion
1120
1362
  //#region src/tools/users.ts
1121
- function registerTools$17(server, apiClient) {
1363
+ function registerTools$21(server, apiClient) {
1122
1364
  registerTool(server, apiClient, "amba_users_list", "List app users for a project. Supports pagination with offset and limit, plus optional search (matches email / external_id / display_name with ILIKE) and segment filter. Returns user profiles including external_id, email, display_name, properties, and activity timestamps.", {
1123
1365
  project_id: z.string().describe("The project ID"),
1124
1366
  limit: z.number().optional().describe("Maximum number of users to return (default 50)"),
@@ -1491,7 +1733,7 @@ final dailyLimit = await Amba.config.get<int>('daily_limit', defaultValue: 10);
1491
1733
  final streak = await Amba.streaks.get('daily_login');
1492
1734
  print('Current streak: \${streak.currentCount}');`
1493
1735
  };
1494
- function registerTools$16(server, apiClient) {
1736
+ function registerTools$20(server, apiClient) {
1495
1737
  registerTool(server, apiClient, "amba_sdk_get_setup_instructions", "Get platform-specific code snippets and setup instructions for integrating the Amba client SDK into a mobile app. Supports iOS (Swift), Android (Kotlin), React Native, Expo, and Flutter.", { platform: z.enum([
1496
1738
  "ios",
1497
1739
  "android",
@@ -1547,7 +1789,7 @@ function registerTools$16(server, apiClient) {
1547
1789
  "1. Replace YOUR_CLIENT_API_KEY with your project's client API key (find it with amba_projects_get)",
1548
1790
  "2. Configure push notification integration (APNs for iOS, FCM for Android) using amba_integrations_configure",
1549
1791
  "3. Create segments to target user groups with amba_segments_create",
1550
- "4. Set up remote config values with amba_configs_create",
1792
+ "4. Set up remote config values with amba_remote_configs_create",
1551
1793
  "5. Create streaks to track user engagement with amba_streaks_create",
1552
1794
  "",
1553
1795
  "## Content delivery loop (worked example — DX-12)",
@@ -1576,7 +1818,7 @@ const criteriaSchema = z.object({
1576
1818
  property_key: z.string().optional().describe("User property key (required for property_value type)"),
1577
1819
  target_value: z.number().describe("Target value to unlock the achievement")
1578
1820
  });
1579
- function registerTools$15(server, apiClient) {
1821
+ function registerTools$19(server, apiClient) {
1580
1822
  registerTool(server, apiClient, "amba_achievements_create", "Create an achievement/badge definition. Achievements unlock automatically when users meet the criteria (e.g. track 5 workouts, reach a 7-day streak, earn 5000 XP). Can optionally award bonus XP on unlock.", {
1581
1823
  project_id: z.string().describe("The project ID"),
1582
1824
  key: z.string().describe("Unique key for this achievement (e.g. \"first_workout\", \"streak_master_7\")"),
@@ -1656,10 +1898,32 @@ function registerTools$15(server, apiClient) {
1656
1898
  text: JSON.stringify(result, null, 2)
1657
1899
  }] };
1658
1900
  }, ["amba_delete_achievement"]);
1901
+ registerTool(server, apiClient, "amba_achievements_grant", "Manually award an achievement to a specific user (marks it unlocked now). For support, promos, or corrections — the rest of the achievement surface only defines criteria the system evaluates automatically. Idempotent: re-granting an already-unlocked achievement keeps the original unlock time. Note: this does NOT auto-award the achievement's xp_reward; award XP separately with amba_xp_grant if intended.", {
1902
+ project_id: z.string().describe("The project ID"),
1903
+ achievement_id: z.string().describe("The achievement definition ID"),
1904
+ app_user_id: z.string().describe("The user to award the achievement to")
1905
+ }, async ({ project_id, achievement_id, app_user_id }, { client }) => {
1906
+ const result = await client.post(`/projects/${project_id}/achievements/${achievement_id}/grant`, { app_user_id });
1907
+ return { content: [{
1908
+ type: "text",
1909
+ text: JSON.stringify(result, null, 2)
1910
+ }] };
1911
+ });
1912
+ registerTool(server, apiClient, "amba_achievements_revoke", "Claw back an achievement from a specific user — the inverse of grant. Removes the user's unlock entirely. Returns 404 if the user does not currently hold the achievement.", {
1913
+ project_id: z.string().describe("The project ID"),
1914
+ achievement_id: z.string().describe("The achievement definition ID"),
1915
+ app_user_id: z.string().describe("The user to revoke the achievement from")
1916
+ }, async ({ project_id, achievement_id, app_user_id }, { client }) => {
1917
+ const result = await client.post(`/projects/${project_id}/achievements/${achievement_id}/revoke`, { app_user_id });
1918
+ return { content: [{
1919
+ type: "text",
1920
+ text: JSON.stringify(result, null, 2)
1921
+ }] };
1922
+ });
1659
1923
  }
1660
1924
  //#endregion
1661
1925
  //#region src/tools/challenges.ts
1662
- function registerTools$14(server, apiClient) {
1926
+ function registerTools$18(server, apiClient) {
1663
1927
  registerTool(server, apiClient, "amba_challenges_create", "Create a time-limited challenge. Challenges are events that run between start_at and end_at, where users try to reach a goal (e.g. track 10 workouts this week, earn 500 XP in 3 days). Can reward XP and/or unlock an achievement on completion.", {
1664
1928
  project_id: z.string().describe("The project ID"),
1665
1929
  name: z.string().describe("Challenge name (e.g. \"7-Day Fitness Sprint\", \"XP Weekend Blitz\")"),
@@ -1770,7 +2034,7 @@ function registerTools$14(server, apiClient) {
1770
2034
  }
1771
2035
  //#endregion
1772
2036
  //#region src/tools/economy.ts
1773
- function registerTools$13(server, apiClient) {
2037
+ function registerTools$17(server, apiClient) {
1774
2038
  registerTool(server, apiClient, "amba_currencies_create", "Create a virtual currency for a project. Currencies can be soft (earned through gameplay) or premium (purchased with real money). Supports auto-recharge for time-gated mechanics like hearts/energy.", {
1775
2039
  project_id: z.string().describe("The project ID"),
1776
2040
  code: z.string().describe("Unique currency code (e.g. \"gold\", \"gems\", \"hearts\")"),
@@ -1824,6 +2088,25 @@ function registerTools$13(server, apiClient) {
1824
2088
  text: JSON.stringify(result, null, 2)
1825
2089
  }] };
1826
2090
  }, ["amba_grant_currency"]);
2091
+ registerTool(server, apiClient, "amba_currencies_spend", "Debit virtual currency from a specific user. Atomic — returns 400 INSUFFICIENT_FUNDS if the balance is too low (no partial debit). Use for in-app purchases priced in soft currency, gameplay consumes, or admin adjustments. The amount parameter is positive; the ledger records the spend as a negative delta automatically.", {
2092
+ project_id: z.string().describe("The project ID"),
2093
+ app_user_id: z.string().describe("The user to debit currency from"),
2094
+ currency_code: z.string().describe("Currency code (e.g. \"gold\")"),
2095
+ amount: z.number().describe("Positive amount to debit. The endpoint records this as a negative delta on the ledger."),
2096
+ reason: z.string().optional().describe("Reason for the spend (stored as reference_id, useful for the audit trail)")
2097
+ }, async ({ project_id, app_user_id, currency_code, amount, reason }, { client }) => {
2098
+ const payload = {
2099
+ app_user_id,
2100
+ currency_code,
2101
+ amount
2102
+ };
2103
+ if (reason !== void 0) payload.reason = reason;
2104
+ const result = await client.post(`/projects/${project_id}/currencies/spend`, payload);
2105
+ return { content: [{
2106
+ type: "text",
2107
+ text: JSON.stringify(result, null, 2)
2108
+ }] };
2109
+ });
1827
2110
  registerTool(server, apiClient, "amba_currency_grant_rules_create", "Create a rule that automatically grants virtual currency when a specific event is tracked. Supports flat-amount grants and property-scaled grants (e.g. points per minute). Optional daily cap prevents farming.", {
1828
2111
  project_id: z.string().describe("The project ID"),
1829
2112
  currency_key: z.string().describe("Currency code to grant (e.g. \"calm_points\")"),
@@ -1978,6 +2261,23 @@ function registerTools$13(server, apiClient) {
1978
2261
  text: JSON.stringify(result, null, 2)
1979
2262
  }] };
1980
2263
  }, ["amba_grant_item"]);
2264
+ registerTool(server, apiClient, "amba_inventory_revoke_item", "Remove a catalog item from a user's inventory — the inverse of grant. Decrements the held quantity; returns 400 INSUFFICIENT_QUANTITY if you try to revoke more than the user holds, and 404 if they don't hold the item at all. Revoking down to zero removes the holding entirely.", {
2265
+ project_id: z.string().describe("The project ID"),
2266
+ app_user_id: z.string().describe("The user to remove the item from"),
2267
+ catalog_item_id: z.string().describe("The catalog item ID to revoke"),
2268
+ quantity: z.number().optional().describe("Quantity to revoke (default 1)")
2269
+ }, async ({ project_id, app_user_id, catalog_item_id, quantity }, { client }) => {
2270
+ const payload = {
2271
+ app_user_id,
2272
+ catalog_item_id
2273
+ };
2274
+ if (quantity !== void 0) payload.quantity = quantity;
2275
+ const result = await client.post(`/projects/${project_id}/inventory/revoke`, payload);
2276
+ return { content: [{
2277
+ type: "text",
2278
+ text: JSON.stringify(result, null, 2)
2279
+ }] };
2280
+ });
1981
2281
  registerTool(server, apiClient, "amba_users_get_inventory", "View a user's inventory including all owned items and quantities.", {
1982
2282
  project_id: z.string().describe("The project ID"),
1983
2283
  app_user_id: z.string().describe("The user ID to look up")
@@ -2266,7 +2566,7 @@ function registerTools$13(server, apiClient) {
2266
2566
  }
2267
2567
  //#endregion
2268
2568
  //#region src/tools/leaderboards.ts
2269
- function registerTools$12(server, apiClient) {
2569
+ function registerTools$16(server, apiClient) {
2270
2570
  registerTool(server, apiClient, "amba_leaderboards_create", "Create a leaderboard definition. Leaderboards rank users by XP, streak length, or a custom metric. Supports all-time, daily, weekly, and monthly periods with configurable max entries.", {
2271
2571
  project_id: z.string().describe("The project ID"),
2272
2572
  name: z.string().describe("Leaderboard name (e.g. \"Top XP Earners\", \"Weekly Streak Leaders\")"),
@@ -2372,7 +2672,7 @@ function registerTools$12(server, apiClient) {
2372
2672
  }
2373
2673
  //#endregion
2374
2674
  //#region src/tools/platform.ts
2375
- function registerTools$11(server, apiClient) {
2675
+ function registerTools$15(server, apiClient) {
2376
2676
  registerTool(server, apiClient, "amba_onboarding_create", "Create an onboarding flow for a project. Onboarding flows define step-by-step experiences for new users (e.g. welcome screens, permission prompts, feature tours).", {
2377
2677
  project_id: z.string().describe("The project ID"),
2378
2678
  name: z.string().describe("Flow name (e.g. \"Welcome Flow\", \"Premium Onboarding\")"),
@@ -2791,6 +3091,44 @@ function registerTools$11(server, apiClient) {
2791
3091
  text: JSON.stringify(result, null, 2)
2792
3092
  }] };
2793
3093
  });
3094
+ registerTool(server, apiClient, "amba_messaging_create_conversation", "Create a conversation server-side. Unlike the client path, this allows ZERO or more initial participants — a matchmaker can create an empty circle then populate it over time. Returns the new conversation row.", {
3095
+ project_id: z.string().describe("The project ID"),
3096
+ type: z.enum(["direct", "group"]).optional().describe("Conversation type. Defaults to \"group\"."),
3097
+ name: z.string().optional().describe("Optional conversation name (group conversations)."),
3098
+ participant_ids: z.array(z.string()).optional().describe("Initial participant app_user IDs. May be empty or omitted.")
3099
+ }, async ({ project_id, type, name, participant_ids }, { client }) => {
3100
+ const payload = {};
3101
+ if (type !== void 0) payload.type = type;
3102
+ if (name !== void 0) payload.name = name;
3103
+ if (participant_ids !== void 0) payload.participant_ids = participant_ids;
3104
+ const result = await client.post(`/projects/${project_id}/messaging/conversations`, payload);
3105
+ return { content: [{
3106
+ type: "text",
3107
+ text: JSON.stringify(result, null, 2)
3108
+ }] };
3109
+ });
3110
+ registerTool(server, apiClient, "amba_messaging_add_participant", "Add a user to an existing conversation (idempotent). The matchmaker primitive: as each user qualifies for a circle, add them to its conversation so they can read, send, and receive messages. Re-adding an existing member is a no-op.", {
3111
+ project_id: z.string().describe("The project ID"),
3112
+ conversation_id: z.string().describe("The conversation ID"),
3113
+ user_id: z.string().describe("The app_user ID to add")
3114
+ }, async ({ project_id, conversation_id, user_id }, { client }) => {
3115
+ const result = await client.post(`/projects/${project_id}/messaging/conversations/${conversation_id}/participants`, { user_id });
3116
+ return { content: [{
3117
+ type: "text",
3118
+ text: JSON.stringify(result, null, 2)
3119
+ }] };
3120
+ });
3121
+ registerTool(server, apiClient, "amba_messaging_remove_participant", "Remove a user from a conversation (idempotent inverse of add). Removing a non-member returns removed: false.", {
3122
+ project_id: z.string().describe("The project ID"),
3123
+ conversation_id: z.string().describe("The conversation ID"),
3124
+ user_id: z.string().describe("The app_user ID to remove")
3125
+ }, async ({ project_id, conversation_id, user_id }, { client }) => {
3126
+ const result = await client.delete(`/projects/${project_id}/messaging/conversations/${conversation_id}/participants/${user_id}`);
3127
+ return { content: [{
3128
+ type: "text",
3129
+ text: JSON.stringify(result, null, 2)
3130
+ }] };
3131
+ });
2794
3132
  registerTool(server, apiClient, "amba_messaging_list_conversations", "List message conversations for a project, newest first. Pagination via limit+offset.", {
2795
3133
  project_id: z.string().describe("The project ID"),
2796
3134
  limit: z.number().optional().describe("Max results to return (default 50)"),
@@ -2910,7 +3248,7 @@ function registerTools$11(server, apiClient) {
2910
3248
  }
2911
3249
  //#endregion
2912
3250
  //#region src/tools/social.ts
2913
- function registerTools$10(server, apiClient) {
3251
+ function registerTools$14(server, apiClient) {
2914
3252
  registerTool(server, apiClient, "amba_friendships_get_stats", "Get friendship statistics for a project. Returns total friendships, pending requests, accepted friendships, and blocked count.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
2915
3253
  const result = await client.get(`/projects/${project_id}/friends/stats`);
2916
3254
  return { content: [{
@@ -3235,7 +3573,7 @@ function registerTools$10(server, apiClient) {
3235
3573
  }
3236
3574
  //#endregion
3237
3575
  //#region src/tools/xp.ts
3238
- function registerTools$9(server, apiClient) {
3576
+ function registerTools$13(server, apiClient) {
3239
3577
  registerTool(server, apiClient, "amba_xp_rules_create", "Create an XP rule that auto-awards XP when a matching engagement event is tracked. For example, award 50 XP every time a user completes a workout, with optional daily caps and cooldowns.", {
3240
3578
  project_id: z.string().describe("The project ID"),
3241
3579
  name: z.string().describe("Rule name (e.g. \"Workout Completed\", \"Daily Login Bonus\")"),
@@ -3307,6 +3645,40 @@ function registerTools$9(server, apiClient) {
3307
3645
  text: JSON.stringify(result, null, 2)
3308
3646
  }] };
3309
3647
  });
3648
+ registerTool(server, apiClient, "amba_xp_grant", "Directly add XP to a user's total — for rewards, promos, or support corrections. This is a manual adjustment: it does NOT replay rule side-effects (level-up events, achievement re-evaluation) the way event-driven XP does. The user's displayed level is still recomputed from the new total. The amount is a positive magnitude; the ledger records it as a positive delta.", {
3649
+ project_id: z.string().describe("The project ID"),
3650
+ app_user_id: z.string().describe("The user to grant XP to"),
3651
+ amount: z.number().describe("Positive amount of XP to add"),
3652
+ reason: z.string().optional().describe("Reason for the grant (stored on the ledger row for the audit trail)")
3653
+ }, async ({ project_id, app_user_id, amount, reason }, { client }) => {
3654
+ const payload = {
3655
+ app_user_id,
3656
+ amount
3657
+ };
3658
+ if (reason !== void 0) payload.reason = reason;
3659
+ const result = await client.post(`/projects/${project_id}/xp/grant`, payload);
3660
+ return { content: [{
3661
+ type: "text",
3662
+ text: JSON.stringify(result, null, 2)
3663
+ }] };
3664
+ });
3665
+ registerTool(server, apiClient, "amba_xp_deduct", "Directly subtract XP from a user's total — the inverse of grant. Atomic: returns 400 INSUFFICIENT_XP if the deduction would drive the total below zero (no partial deduct). Like grant, this is a manual adjustment that does NOT replay rule side-effects. The amount is a positive magnitude; the ledger records it as a negative delta.", {
3666
+ project_id: z.string().describe("The project ID"),
3667
+ app_user_id: z.string().describe("The user to deduct XP from"),
3668
+ amount: z.number().describe("Positive amount of XP to subtract"),
3669
+ reason: z.string().optional().describe("Reason for the deduction (stored on the ledger row for the audit trail)")
3670
+ }, async ({ project_id, app_user_id, amount, reason }, { client }) => {
3671
+ const payload = {
3672
+ app_user_id,
3673
+ amount
3674
+ };
3675
+ if (reason !== void 0) payload.reason = reason;
3676
+ const result = await client.post(`/projects/${project_id}/xp/deduct`, payload);
3677
+ return { content: [{
3678
+ type: "text",
3679
+ text: JSON.stringify(result, null, 2)
3680
+ }] };
3681
+ });
3310
3682
  registerTool(server, apiClient, "amba_xp_list_users", "List users by total XP (leaderboard view). Ordered by total_xp descending with pagination.", {
3311
3683
  project_id: z.string().describe("The project ID"),
3312
3684
  limit: z.number().optional().describe("Max results to return (default 50)"),
@@ -3324,7 +3696,7 @@ function registerTools$9(server, apiClient) {
3324
3696
  }
3325
3697
  //#endregion
3326
3698
  //#region src/tools/events.ts
3327
- function registerTools$8(server, apiClient) {
3699
+ function registerTools$12(server, apiClient) {
3328
3700
  registerTool(server, apiClient, "amba_events_list", "List engagement events for a project (most recent first) with cursor pagination on (occurred_at, id). Defaults to the last 24 hours when `since` is omitted. Pass `next_cursor` from a previous page back as `cursor` to continue.", {
3329
3701
  project_id: z.string().describe("The project ID"),
3330
3702
  since: z.string().optional().describe("ISO-8601 lower bound for occurred_at. Defaults to 24h ago."),
@@ -3366,6 +3738,125 @@ function registerTools$8(server, apiClient) {
3366
3738
  });
3367
3739
  }
3368
3740
  //#endregion
3741
+ //#region src/tools/funnels.ts
3742
+ const funnelFilterSchema = z.object({
3743
+ property: z.string().describe("Dot-path into the event `properties` JSON (e.g. \"source\" or \"plan.tier\")."),
3744
+ op: z.enum([
3745
+ "eq",
3746
+ "neq",
3747
+ "gt",
3748
+ "gte",
3749
+ "lt",
3750
+ "lte",
3751
+ "contains",
3752
+ "not_contains",
3753
+ "exists",
3754
+ "not_exists"
3755
+ ]).describe("Comparison operator. exists/not_exists ignore `value`."),
3756
+ value: z.union([
3757
+ z.string(),
3758
+ z.number(),
3759
+ z.boolean()
3760
+ ]).optional().describe("Scalar to compare against. Required for every op except exists/not_exists.")
3761
+ });
3762
+ const funnelStepSchema = z.object({
3763
+ event: z.string().describe("Event name to match (engagement event_name)."),
3764
+ filters: z.array(funnelFilterSchema).optional().describe("Optional property predicates, ANDed together, that an event must satisfy.")
3765
+ });
3766
+ function registerTools$11(server, apiClient) {
3767
+ registerTool(server, apiClient, "amba_funnels_list", "List conversion funnels for a project. A funnel is a saved, ordered sequence of steps (event + optional property filters) plus a conversion window. Returns the definitions only — use amba_funnels_query to compute per-step counts.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
3768
+ const result = await client.get(`/projects/${project_id}/funnels`);
3769
+ return { content: [{
3770
+ type: "text",
3771
+ text: JSON.stringify(result, null, 2)
3772
+ }] };
3773
+ });
3774
+ registerTool(server, apiClient, "amba_funnels_create", [
3775
+ "Create a conversion funnel definition. Steps are ORDERED: a user is credited with",
3776
+ "step N only if they emitted step N after their matched step N-1, all within the",
3777
+ "conversion window measured from their first step-0 event. conversion_window_seconds",
3778
+ "defaults to 604800 (7 days). Needs at least 2 steps."
3779
+ ].join(" "), {
3780
+ project_id: z.string().describe("The project ID"),
3781
+ name: z.string().describe("Funnel name (unique per project)."),
3782
+ description: z.string().optional().describe("Human-readable description."),
3783
+ steps: z.array(funnelStepSchema).describe("Ordered steps (2-10). Each step is an event + optional property filters."),
3784
+ conversion_window_seconds: z.number().int().optional().describe("Conversion window in seconds (60 .. 7776000). Defaults to 604800 (7 days).")
3785
+ }, async ({ project_id, name, description, steps, conversion_window_seconds }, { client }) => {
3786
+ const payload = {
3787
+ name,
3788
+ steps
3789
+ };
3790
+ if (description !== void 0) payload.description = description;
3791
+ if (conversion_window_seconds !== void 0) payload.conversion_window_seconds = conversion_window_seconds;
3792
+ const result = await client.post(`/projects/${project_id}/funnels`, payload);
3793
+ return { content: [{
3794
+ type: "text",
3795
+ text: JSON.stringify(result, null, 2)
3796
+ }] };
3797
+ });
3798
+ registerTool(server, apiClient, "amba_funnels_get", "Get a single funnel definition by id (name, ordered steps, conversion window).", {
3799
+ project_id: z.string().describe("The project ID"),
3800
+ funnel_id: z.string().describe("The funnel ID")
3801
+ }, async ({ project_id, funnel_id }, { client }) => {
3802
+ const result = await client.get(`/projects/${project_id}/funnels/${funnel_id}`);
3803
+ return { content: [{
3804
+ type: "text",
3805
+ text: JSON.stringify(result, null, 2)
3806
+ }] };
3807
+ });
3808
+ registerTool(server, apiClient, "amba_funnels_update", "Update a funnel's name, description, steps, and/or conversion window. Only the provided fields change; steps (when supplied) replace the whole ordered list.", {
3809
+ project_id: z.string().describe("The project ID"),
3810
+ funnel_id: z.string().describe("The funnel ID"),
3811
+ name: z.string().optional().describe("New funnel name."),
3812
+ description: z.string().nullable().optional().describe("New description; pass null to clear."),
3813
+ steps: z.array(funnelStepSchema).optional().describe("Replacement ordered step list (2-10)."),
3814
+ conversion_window_seconds: z.number().int().optional().describe("New conversion window in seconds (60 .. 7776000).")
3815
+ }, async ({ project_id, funnel_id, name, description, steps, conversion_window_seconds }, { client }) => {
3816
+ const payload = {};
3817
+ if (name !== void 0) payload.name = name;
3818
+ if (description !== void 0) payload.description = description;
3819
+ if (steps !== void 0) payload.steps = steps;
3820
+ if (conversion_window_seconds !== void 0) payload.conversion_window_seconds = conversion_window_seconds;
3821
+ const result = await client.patch(`/projects/${project_id}/funnels/${funnel_id}`, payload);
3822
+ return { content: [{
3823
+ type: "text",
3824
+ text: JSON.stringify(result, null, 2)
3825
+ }] };
3826
+ });
3827
+ registerTool(server, apiClient, "amba_funnels_delete", "Delete a funnel definition by id.", {
3828
+ project_id: z.string().describe("The project ID"),
3829
+ funnel_id: z.string().describe("The funnel ID")
3830
+ }, async ({ project_id, funnel_id }, { client }) => {
3831
+ const result = await client.delete(`/projects/${project_id}/funnels/${funnel_id}`);
3832
+ return { content: [{
3833
+ type: "text",
3834
+ text: JSON.stringify(result, null, 2)
3835
+ }] };
3836
+ });
3837
+ registerTool(server, apiClient, "amba_funnels_query", [
3838
+ "Compute a funnel: per-step distinct-user counts, conversion rates, and drop-off over",
3839
+ "an entry date range. `from`/`to` (ISO-8601) bound which step-0 events count as funnel",
3840
+ "entries (default: last 30 days). Each entrant then has their own conversion window in",
3841
+ "which later steps must occur (it may extend past `to`). Returns total_entered,",
3842
+ "total_converted, overall_conversion_rate, and a per-step breakdown."
3843
+ ].join(" "), {
3844
+ project_id: z.string().describe("The project ID"),
3845
+ funnel_id: z.string().describe("The funnel ID"),
3846
+ from: z.string().optional().describe("ISO-8601 lower bound for funnel entries. Defaults to 30 days ago."),
3847
+ to: z.string().optional().describe("ISO-8601 upper bound for funnel entries. Defaults to now.")
3848
+ }, async ({ project_id, funnel_id, from, to }, { client }) => {
3849
+ const query = {};
3850
+ if (from !== void 0) query.from = from;
3851
+ if (to !== void 0) query.to = to;
3852
+ const result = await client.get(`/projects/${project_id}/funnels/${funnel_id}/query`, query);
3853
+ return { content: [{
3854
+ type: "text",
3855
+ text: JSON.stringify(result, null, 2)
3856
+ }] };
3857
+ });
3858
+ }
3859
+ //#endregion
3369
3860
  //#region src/lib/tool-result.ts
3370
3861
  /**
3371
3862
  * Shared MCP tool result helpers.
@@ -3538,7 +4029,7 @@ function enrichedAuthResult(result, agentInstructions) {
3538
4029
  status: result.status
3539
4030
  });
3540
4031
  }
3541
- function registerTools$7(server, apiClient) {
4032
+ function registerTools$10(server, apiClient) {
3542
4033
  registerPublicTool(server, "amba_developer_signup", [
3543
4034
  "Create a new Amba developer account. Returns a long-lived Personal Access Token (PAT)",
3544
4035
  "plus a real isolated Amba project (provisioning asynchronously), plus ready-to-paste",
@@ -3735,7 +4226,7 @@ const sdkOrderSchema = z.array(z.object({
3735
4226
  })).describe("Ordering. `[{column: \"created_at\", direction: \"desc\"}, ...]`.");
3736
4227
  const adminFindWhereSchema = z.unknown().describe("Server WhereClause shape: column-keyed FieldOps, e.g. {tag: {eq: \"x\"}, count: {gte: 5}} plus optional `and|or|not` combinators.");
3737
4228
  const setSchema = z.record(z.unknown()).describe("Column-value map. Server-managed columns (id, created_at, etc.) are rejected.");
3738
- function registerTools$6(server, apiClient) {
4229
+ function registerTools$9(server, apiClient) {
3739
4230
  registerTool(server, apiClient, "amba_collections_create", [
3740
4231
  "Create a new collection (schema-first Postgres table) in a project.",
3741
4232
  "The DDL is emitted server-side and applied via a Temporal saga.",
@@ -3746,13 +4237,15 @@ function registerTools$6(server, apiClient) {
3746
4237
  project_id: z.string().describe("The project ID owning the collection."),
3747
4238
  name: z.string().describe("Collection name. Must match /^[a-z][a-z0-9_]*$/. Reserved prefixes (amba_, coll_amba_) are rejected."),
3748
4239
  columns: z.array(columnSchema).describe("Column definitions. Server adds id/user_id/created_at/updated_at/deleted_at."),
3749
- indexes: z.array(indexSchema).optional().describe("Optional extra indexes (server creates a primary key + user_id index by default).")
3750
- }, async ({ project_id, name, columns, indexes }, { pat }) => {
4240
+ indexes: z.array(indexSchema).optional().describe("Optional extra indexes (server creates a primary key + user_id index by default)."),
4241
+ shared: z.boolean().optional().describe("Set true for developer-seeded GLOBAL content not owned by any single user (question banks, content libraries, lookup tables). Makes user_id NULLABLE so rows can be inserted without a user_id. Defaults to false (every row requires a user_id).")
4242
+ }, async ({ project_id, name, columns, indexes, shared }, { pat }) => {
3751
4243
  const body = {
3752
4244
  name,
3753
4245
  columns
3754
4246
  };
3755
4247
  if (indexes !== void 0) body.indexes = indexes;
4248
+ if (shared !== void 0) body.shared = shared;
3756
4249
  return passthroughResult(await adminFetch(apiClient, {
3757
4250
  method: "POST",
3758
4251
  path: `/projects/${project_id}/collections`,
@@ -3810,22 +4303,39 @@ function registerTools$6(server, apiClient) {
3810
4303
  }));
3811
4304
  }, ["amba_delete_collection"]);
3812
4305
  registerTool(server, apiClient, "amba_collections_alter", [
3813
- "Alter a collection: exactly one of `add_column`, `add_index`, or `drop_column` per call (the underlying saga is built around one SQL blob per workflow).",
4306
+ "Alter a collection: exactly one of `add_column`, `add_index`, `change_type`, `rename_column`, `drop_column`, or `relax_user_id` per call (the underlying saga is built around one SQL blob per workflow).",
3814
4307
  "Drop-column is destructive — requires `confirm` to equal the dropped column name.",
4308
+ "change_type changes an existing column to a new type. It REWRITES THE WHOLE TABLE under an exclusive lock — the collection is briefly unreadable/unwritable while it runs, so prefer low-traffic windows for large collections. Casts that are not trivially safe (e.g. text→integer) require a `using` cast expression; known-safe casts (anything→text, integer→bigint, integer/bigint→numeric) do not. Without a `using` for an unsafe cast you get a 400 up front rather than a mid-migration failure.",
4309
+ "rename_column renames a column (metadata-only, fast). It BREAKS any app code referencing the old name — re-run `amba types generate` and update call sites afterward. The new name can’t collide with a server-managed column (id, user_id, created_at, updated_at, deleted_at) or a reserved query keyword (and, or, not).",
4310
+ "relax_user_id converts an EXISTING collection to shared (drops the user_id NOT NULL so global rows can be inserted) — the post-hoc equivalent of creating it with shared:true.",
3815
4311
  "Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
3816
- "Errors: 400 INVALID_PATCH if not exactly one operation, 400 CONFIRMATION_REQUIRED on drop_column without matching confirm."
4312
+ "Errors: 400 INVALID_PATCH if not exactly one operation, 400 CONFIRMATION_REQUIRED on drop_column without matching confirm, 400 INVALID_COLLECTION_SCHEMA on an unsafe change_type cast or a reserved rename target, 400 COLLECTION_MIGRATION_FAILED if the cast fails at apply time."
3817
4313
  ].join(" "), {
3818
4314
  project_id: z.string().describe("The project ID."),
3819
4315
  name: z.string().describe("Collection name to alter."),
3820
- add_column: columnSchema.optional().describe("Column to add. Mutually exclusive with add_index / drop_column."),
3821
- add_index: indexSchema.optional().describe("Index to add. Mutually exclusive with add_column / drop_column."),
4316
+ add_column: columnSchema.optional().describe("Column to add. Mutually exclusive with the other alter operations."),
4317
+ add_index: indexSchema.optional().describe("Index to add. Mutually exclusive with the other alter operations."),
4318
+ change_type: z.object({
4319
+ column: z.string().describe("Existing column to change."),
4320
+ to_type: z.string().describe("Target type (text, integer, bigint, numeric, boolean, timestamptz, date, jsonb, uuid)."),
4321
+ from_type: z.string().optional().describe("Optional current type. When provided, known-safe widening casts (integer→bigint, integer/bigint→numeric) skip the `using` requirement."),
4322
+ using: z.string().optional().describe("SQL cast expression for the USING clause, e.g. \"score::integer\". Required for casts that are not trivially safe; omit only for anything→text or a known-safe widening cast.")
4323
+ }).optional().describe("Change a column’s type. REWRITES THE WHOLE TABLE under an exclusive lock (briefly unreadable/unwritable). Mutually exclusive with the other alter operations."),
4324
+ rename_column: z.object({
4325
+ from: z.string().describe("Current column name."),
4326
+ to: z.string().describe("New column name. Must match /^[a-z][a-z0-9_]*$/.")
4327
+ }).optional().describe("Rename a column (metadata-only, fast). BREAKS code referencing the old name — re-run `amba types generate` afterward. Mutually exclusive with the other alter operations."),
3822
4328
  drop_column: z.string().optional().describe("Column name to drop. Requires `confirm` query param to match."),
4329
+ relax_user_id: z.boolean().optional().describe("Set true to drop the user_id NOT NULL constraint so this collection can hold shared/global rows (NULL user_id). Mutually exclusive with the other operations."),
3823
4330
  confirm: z.string().optional().describe("Required when drop_column is set; must equal the column name.")
3824
- }, async ({ project_id, name, add_column, add_index, drop_column, confirm }, { pat }) => {
4331
+ }, async ({ project_id, name, add_column, add_index, change_type, rename_column, drop_column, relax_user_id, confirm }, { pat }) => {
3825
4332
  const body = {};
3826
4333
  if (add_column !== void 0) body.add_column = add_column;
3827
4334
  if (add_index !== void 0) body.add_index = add_index;
4335
+ if (change_type !== void 0) body.change_type = change_type;
4336
+ if (rename_column !== void 0) body.rename_column = rename_column;
3828
4337
  if (drop_column !== void 0) body.drop_column = drop_column;
4338
+ if (relax_user_id !== void 0) body.relax_user_id = relax_user_id;
3829
4339
  const query = {};
3830
4340
  if (drop_column !== void 0 && confirm !== void 0) query.confirm = confirm;
3831
4341
  return passthroughResult(await adminFetch(apiClient, {
@@ -3853,6 +4363,28 @@ function registerTools$6(server, apiClient) {
3853
4363
  bearer: pat
3854
4364
  }));
3855
4365
  });
4366
+ registerTool(server, apiClient, "amba_admin_insert_rows", [
4367
+ "Bulk-insert up to 500 rows into a collection in a single atomic statement — BYPASSES auto-RLS.",
4368
+ "Use for developer-seeded data (question banks, content libraries, lookup tables); far faster than calling amba_admin_insert_row in a loop.",
4369
+ "Server-managed columns (id, created_at, updated_at, deleted_at) are stripped per row; `user_id` is honored if present. Rows may be sparse — a column absent from a row is inserted as NULL.",
4370
+ "on_conflict \"error\" (default) aborts the whole batch on any conflict; \"skip\" emits ON CONFLICT DO NOTHING and reports how many rows were skipped.",
4371
+ "Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
4372
+ "Errors: 400 BATCH_TOO_LARGE (>500) / INVALID_COLUMN / NOT_NULL_VIOLATION, 404 COLLECTION_NOT_FOUND, 409 CONFLICT (on_conflict:\"error\")."
4373
+ ].join(" "), {
4374
+ project_id: z.string().describe("The project ID."),
4375
+ name: z.string().describe("Collection name."),
4376
+ rows: z.array(z.record(z.unknown())).min(1).max(500).describe("1–500 column-value maps. Server-managed columns are stripped per row."),
4377
+ on_conflict: z.enum(["error", "skip"]).optional().describe("Conflict policy. \"error\" (default) fails the batch; \"skip\" ignores conflicts.")
4378
+ }, async ({ project_id, name, rows, on_conflict }, { pat }) => {
4379
+ const body = { rows };
4380
+ if (on_conflict !== void 0) body.on_conflict = on_conflict;
4381
+ return passthroughResult(await adminFetch(apiClient, {
4382
+ method: "POST",
4383
+ path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows:batch`,
4384
+ body,
4385
+ bearer: pat
4386
+ }));
4387
+ });
3856
4388
  registerTool(server, apiClient, "amba_admin_list_rows", [
3857
4389
  "List rows in a collection from the admin side. BYPASSES auto-RLS — every row in the collection is visible.",
3858
4390
  "Default behavior includes soft-deleted rows (devs debugging \"where did the row go\" need them); pass `include_deleted=false` to hide them.",
@@ -4232,7 +4764,7 @@ function jsonResult(payload) {
4232
4764
  * of a generic upstream 400.
4233
4765
  */
4234
4766
  const MAX_BUNDLE_BYTES = 10 * 1024 * 1024;
4235
- function registerTools$5(server, apiClient) {
4767
+ function registerTools$8(server, apiClient) {
4236
4768
  registerTool(server, apiClient, "amba_functions_deploy", [
4237
4769
  "Deploy a function to an Amba project. The supplied `code` is a fully bundled JavaScript module (one entry file) — the deploy endpoint uploads it server-side and records a new active deployment row, superseding any prior active version for the same `name`.",
4238
4770
  "Function name must match /^[a-z][a-z0-9_-]*$/ and be ≤58 chars. Bundle size cap: 10 MiB (enforced client-side too — oversize uploads are rejected before the round-trip).",
@@ -4357,7 +4889,7 @@ function registerTools$5(server, apiClient) {
4357
4889
  }
4358
4890
  //#endregion
4359
4891
  //#region src/tools/sites.ts
4360
- function registerTools$4(server, apiClient) {
4892
+ function registerTools$7(server, apiClient) {
4361
4893
  registerTool(server, apiClient, "amba_sites_deploy", [
4362
4894
  "Deploy a static site to an Amba project. Creates the site if it does not yet exist, then uploads the supplied files as a new deployment.",
4363
4895
  "The deployment is immutable — its files live forever under the site's storage prefix — and the site flips to serve the new deployment atomically. Older deployments are kept for rollback via `amba_sites_list`.",
@@ -4448,22 +4980,81 @@ function registerTools$4(server, apiClient) {
4448
4980
  }, ["amba_remove_site_domain"]);
4449
4981
  }
4450
4982
  //#endregion
4983
+ //#region src/tools/domains.ts
4984
+ function registerTools$6(server, apiClient) {
4985
+ registerTool(server, apiClient, "amba_domains_search", ["Search for available domains to buy through Amba. Pass a keyword or a full domain; returns candidate domains with availability and first-year price (USD).", "Free — searching never costs anything. Use this to help a user pick a name, then `amba_domains_check` to price specific picks and `amba_domains_purchase` to buy."].join(" "), {
4986
+ project_id: z.string().describe("The Amba project ID."),
4987
+ query: z.string().describe("A keyword (e.g. \"getunbury\") or a full domain (e.g. \"unbury.com\")."),
4988
+ limit: z.number().int().min(1).max(50).optional().describe("Max suggestions (1-50, default 20).")
4989
+ }, async ({ project_id, query, limit }, { pat }) => {
4990
+ const q = { q: query };
4991
+ if (limit !== void 0) q.limit = String(limit);
4992
+ return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/domains/search`, { query: q }));
4993
+ });
4994
+ registerTool(server, apiClient, "amba_domains_check", ["Check authoritative availability + price for specific domains (up to 20 at once). Free.", "Returns, per domain: whether it is available, the first-year price (USD), whether it is a premium name, and whether Amba can register that extension."].join(" "), {
4995
+ project_id: z.string().describe("The Amba project ID."),
4996
+ domains: z.array(z.string()).min(1).max(20).describe("Domains to check, e.g. [\"unbury.com\", \"unbury.app\"]. Lowercased at the boundary.")
4997
+ }, async ({ project_id, domains }, { pat }) => {
4998
+ const normalised = domains.map((d) => d.toLowerCase());
4999
+ return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/domains/check`, { body: { domains: normalised } }));
5000
+ });
5001
+ registerTool(server, apiClient, "amba_domains_purchase", [
5002
+ "Buy a domain through Amba and connect it to one of the project's sites as a live custom domain — the customer never touches DNS.",
5003
+ "TWO-STEP / MONEY SAFETY: the first call (without `confirm`) returns a QUOTE with the price and `confirmation_required: true` and charges nothing. Surface the price to the user and get their go-ahead, then call again with `confirm: true` and `accept_price_usd` set to the quoted price to execute the purchase.",
5004
+ "A real domain registration spends money and is permanent. If purchasing is not enabled on the deployment, the call returns the quote with `gated: true` and still charges nothing.",
5005
+ "On success: the domain is registered, DNS is auto-configured, and the site begins serving on the new domain (the TLS certificate issues automatically)."
5006
+ ].join(" "), {
5007
+ project_id: z.string().describe("The Amba project ID."),
5008
+ domain: z.string().describe("The domain to buy, e.g. \"unbury.com\". Must be available (check first)."),
5009
+ site: z.string().describe("The site name to connect the domain to (from amba_sites_list)."),
5010
+ accept_price_usd: z.number().optional().describe("Required to execute: the price (USD) you were quoted. Omit on the first (quote) call."),
5011
+ confirm: z.boolean().optional().describe("Set true ONLY after the user has approved the cost. Omit on the first (quote) call."),
5012
+ auto_renew: z.boolean().optional().describe("Auto-renew the domain each year (default true)."),
5013
+ privacy: z.boolean().optional().describe("Enable WHOIS privacy (default true)."),
5014
+ years: z.number().int().min(1).max(10).optional().describe("Registration length in years (default 1).")
5015
+ }, async ({ project_id, domain, site, accept_price_usd, confirm, auto_renew, privacy, years }, { pat }) => {
5016
+ const body = {
5017
+ domain: domain.toLowerCase(),
5018
+ site
5019
+ };
5020
+ if (accept_price_usd !== void 0) body.accept_price_usd = accept_price_usd;
5021
+ if (confirm !== void 0) body.confirm = confirm;
5022
+ if (auto_renew !== void 0) body.auto_renew = auto_renew;
5023
+ if (privacy !== void 0) body.privacy = privacy;
5024
+ if (years !== void 0) body.years = years;
5025
+ return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/domains/purchase`, { body }));
5026
+ });
5027
+ registerTool(server, apiClient, "amba_domains_list", "List domains this project has purchased through Amba, with registration status and the site each is connected to.", {
5028
+ project_id: z.string().describe("The Amba project ID."),
5029
+ limit: z.number().int().min(1).max(200).optional().describe("Page size (1-200, default 50)."),
5030
+ offset: z.number().int().min(0).optional().describe("Skip rows (default 0).")
5031
+ }, async ({ project_id, limit, offset }, { pat }) => {
5032
+ const query = {};
5033
+ if (limit !== void 0) query.limit = String(limit);
5034
+ if (offset !== void 0) query.offset = String(offset);
5035
+ return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/domains`, { query }));
5036
+ });
5037
+ }
5038
+ //#endregion
4451
5039
  //#region src/tools/secrets.ts
4452
- function registerTools$3(server, apiClient) {
5040
+ function registerTools$5(server, apiClient) {
4453
5041
  registerTool(server, apiClient, "amba_secrets_set", [
4454
- "Set or rotate a function-scoped secret. Every secret belongs to exactly one function — the `function` arg is required.",
4455
- "Writes a new version to the backing secret store and enqueues a Workers-side sync. Subsequent function invocations see the new value once sync_status flips to \"synced\" (typically <30s).",
4456
- "Secret name must match /^[A-Z][A-Z0-9_]{0,62}$/. Function name must match /^[a-z][a-z0-9_-]{0,57}$/.",
5042
+ "Set or rotate a secret. Omit `function` for a PROJECT-WIDE secret (one value visible to every function in the project, including ones deployed later) — set it once instead of repeating per function. Pass `function` to scope the secret to just that function.",
5043
+ "Writes a new version to the backing secret store and enqueues the sync. Subsequent function invocations see the new value once sync_status flips to \"synced\" (typically <30s).",
5044
+ "Secret name must match /^[A-Z][A-Z0-9_]{0,62}$/. Function name (when given) must match /^[a-z][a-z0-9_-]{0,57}$/.",
4457
5045
  "Reserved binding names (AMBA_INTERNAL_TOKEN, EDGE_HEADER_SIGNING_SECRET, STORAGE, EDGE_DB_PROXY, AMBA_API_URL, AMBA_AI_GATEWAY_URL, AMBA_PROJECT_ID) are platform-managed and rejected with 400 RESERVED_BINDING.",
4458
- "The function must already have at least one deployment row (deploy it first via `amba_functions_deploy`).",
5046
+ "Secrets may be set BEFORE deploying the function — the sync drains once a deployment lands, so you can set every secret first and deploy once.",
4459
5047
  "Value cap: 64 KiB. Plaintext is never returned afterwards."
4460
5048
  ].join(" "), {
4461
5049
  project_id: z.string().describe("The Amba project ID."),
4462
5050
  name: z.string().describe("Secret name. UPPER_SNAKE_CASE. /^[A-Z][A-Z0-9_]{0,62}$/."),
4463
5051
  value: z.string().describe("Secret value. Stored encrypted at rest; never returned in plaintext afterwards."),
4464
- function: z.string().describe("Function name this secret binds to. Required — secrets are function-scoped.")
5052
+ function: z.string().optional().describe("Function this secret binds to. OMIT for a project-wide secret (applied to every function in the project).")
4465
5053
  }, async ({ project_id, name, value, function: functionName }, { pat }) => {
4466
- return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/secrets`, { body: {
5054
+ return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/secrets`, { body: functionName === void 0 ? {
5055
+ name,
5056
+ value
5057
+ } : {
4467
5058
  name,
4468
5059
  value,
4469
5060
  function: functionName
@@ -4488,25 +5079,26 @@ function registerTools$3(server, apiClient) {
4488
5079
  return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/secrets`));
4489
5080
  }, ["amba_list_secrets"]);
4490
5081
  registerTool(server, apiClient, "amba_secrets_delete", [
4491
- "Delete a function-scoped secret. Removes the version from the backing store AND the sync-pending row. The Workers-side binding is reaped on the next deploy of the function (the binding list is re-emitted without the secret).",
4492
- "The `function` arg is required — secrets are function-scoped.",
5082
+ "Delete a secret. Removes the version from the backing store AND the sync-pending row. The Worker-side binding is reaped on the next deploy of the affected function(s) (the binding list is re-emitted without the secret).",
5083
+ "Pass `function` to delete a function-scoped secret; OMIT it to delete the project-wide secret of this name.",
4493
5084
  "Idempotent: deleting a secret that no longer exists succeeds."
4494
5085
  ].join(" "), {
4495
5086
  project_id: z.string().describe("The Amba project ID."),
4496
5087
  name: z.string().describe("Secret name. /^[A-Z][A-Z0-9_]{0,62}$/."),
4497
- function: z.string().describe("Function name this secret was bound to.")
5088
+ function: z.string().optional().describe("Function this secret was bound to. OMIT to delete the project-wide secret.")
4498
5089
  }, async ({ project_id, name, function: functionName }, { pat }) => {
5090
+ const scope = functionName ?? null;
4499
5091
  try {
4500
- return jsonResult(await callWithPat(apiClient, pat, "DELETE", `/projects/${encodeURIComponent(project_id)}/secrets/${encodeURIComponent(name)}`, { query: { function: functionName } }) ?? {
5092
+ return jsonResult(await callWithPat(apiClient, pat, "DELETE", `/projects/${encodeURIComponent(project_id)}/secrets/${encodeURIComponent(name)}`, functionName === void 0 ? {} : { query: { function: functionName } }) ?? {
4501
5093
  deleted: true,
4502
5094
  name,
4503
- function: functionName
5095
+ function: scope
4504
5096
  });
4505
5097
  } catch (err) {
4506
5098
  if (err instanceof AmbaApiError && err.status === 404) return jsonResult({
4507
5099
  deleted: true,
4508
5100
  name,
4509
- function: functionName,
5101
+ function: scope,
4510
5102
  already_absent: true
4511
5103
  });
4512
5104
  throw err;
@@ -4521,7 +5113,7 @@ const RATE_LIMIT_SHAPE = z.object({
4521
5113
  max: z.number().int().min(1).optional().describe("Max invocations per window. Defaults to 20."),
4522
5114
  key: z.string().optional().describe("Bucketing key. Typically \"user_id\" or \"session_id\". Defaults to \"user_id\".")
4523
5115
  }).strict();
4524
- function registerTools$2(server, apiClient) {
5116
+ function registerTools$4(server, apiClient) {
4525
5117
  registerTool(server, apiClient, "amba_ai_prompts_create", [
4526
5118
  "Register a new AI prompt template on a project. Returns the persisted prompt row (name, version, provider, model, client_invokable).",
4527
5119
  "Name must match /^[a-z][a-z0-9_-]{0,127}$/. Provider must be \"anthropic\" or \"openai\" and must already be registered (POST /ai/providers — currently CLI / console). Model is the provider-native model id (e.g. \"claude-sonnet-4-5-20250929\", \"gpt-4o-2024-08-06\").",
@@ -4723,7 +5315,7 @@ const TIER_CATALOG = {
4723
5315
  note: "Use Amba.track(name, props, { telemetry: true }) for high-volume telemetry. Cheaper, no fan-out to segments/workflows/push."
4724
5316
  }
4725
5317
  };
4726
- function registerTools$1(server, apiClient) {
5318
+ function registerTools$3(server, apiClient) {
4727
5319
  registerTool(server, apiClient, "amba_billing_status", "Get the current billing tier, headroom on each metered axis (MAU, events, push, db_storage, media_storage), projected overage for the current period, spend ceiling, and what (if any) human action is required before the agent can continue safely. Use this BEFORE provisioning new features or running data-heavy workloads so the agent can self-throttle or escalate to the human.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
4728
5320
  const result = await client.get(`/projects/${project_id}/billing/status`);
4729
5321
  return { content: [{
@@ -4750,7 +5342,7 @@ function registerTools$1(server, apiClient) {
4750
5342
  }
4751
5343
  //#endregion
4752
5344
  //#region src/tools/invites.ts
4753
- function registerTools(server, apiClient) {
5345
+ function registerTools$2(server, apiClient) {
4754
5346
  registerTool(server, apiClient, "amba_projects_invite_member", "Invite a teammate by email to collaborate on this project. Returns the invite URL the human should send to their teammate. Roles: admin (can manage members + integrations), member (can write data), viewer (read-only).", {
4755
5347
  project_id: z.string().describe("The project ID"),
4756
5348
  email: z.string().describe("Teammate's email address"),
@@ -4777,7 +5369,7 @@ function registerTools(server, apiClient) {
4777
5369
  text: JSON.stringify(result, null, 2)
4778
5370
  }] };
4779
5371
  });
4780
- registerTool(server, apiClient, "amba_projects_remove_member", "Remove a member from a project. The owner cannot be removed (transfer ownership first via amba_projects_transfer_owner — not yet implemented). Members can remove themselves.", {
5372
+ registerTool(server, apiClient, "amba_projects_remove_member", "Remove a member from a project. The owner cannot be removed — transfer ownership first via amba_projects_transfer_owner, then remove the demoted ex-owner like any admin. Members can remove themselves.", {
4781
5373
  project_id: z.string().describe("The project ID"),
4782
5374
  developer_id: z.string().describe("The developer ID to remove")
4783
5375
  }, async ({ project_id, developer_id }, { client }) => {
@@ -4787,6 +5379,304 @@ function registerTools(server, apiClient) {
4787
5379
  text: JSON.stringify(result, null, 2)
4788
5380
  }] };
4789
5381
  });
5382
+ registerTool(server, apiClient, "amba_projects_transfer_owner", "Transfer project ownership to an existing member. Only the current owner can do this. The new owner must already be a member (invite + accept them first if not). The previous owner is automatically demoted to admin, retaining member and integration management. Use this before removing the current owner — owners cannot be removed directly.", {
5383
+ project_id: z.string().describe("The project ID"),
5384
+ new_owner_developer_id: z.string().describe("Developer ID of the existing member who should become the new owner.")
5385
+ }, async ({ project_id, new_owner_developer_id }, { client }) => {
5386
+ const result = await client.post(`/projects/${project_id}/transfer-ownership`, { new_owner_developer_id });
5387
+ return { content: [{
5388
+ type: "text",
5389
+ text: JSON.stringify(result, null, 2)
5390
+ }] };
5391
+ });
5392
+ }
5393
+ //#endregion
5394
+ //#region src/tools/webhooks.ts
5395
+ function registerTools$1(server, apiClient) {
5396
+ registerTool(server, apiClient, "amba_webhooks_create", "Register an outbound webhook subscription. Amba sends an HMAC-signed POST to target_url every time an event named event_name fires. The signing secret is returned ONCE in this response — copy it into your receiver config; later reads never include it. target_url must be https:// (http:// is allowed only for localhost during development).", {
5397
+ project_id: z.string().describe("The project ID"),
5398
+ event_name: z.string().describe("Event name to subscribe to. Must match ^[a-z][a-z0-9_.-]{0,127}$ (case-insensitive)."),
5399
+ target_url: z.string().describe("HTTPS endpoint that receives the signed POST. http:// allowed only for localhost.")
5400
+ }, async ({ project_id, event_name, target_url }, { client }) => {
5401
+ const result = await client.post(`/projects/${project_id}/webhooks/subscriptions`, {
5402
+ event_name,
5403
+ target_url
5404
+ });
5405
+ return { content: [{
5406
+ type: "text",
5407
+ text: JSON.stringify(result, null, 2)
5408
+ }] };
5409
+ });
5410
+ registerTool(server, apiClient, "amba_webhooks_list", "List all webhook subscriptions for a project (most recent first). Secrets are never included — only id, event_name, target_url, status, and timestamps. status is \"active\", \"degraded\" (the delivery circuit breaker tripped on repeated failures), or \"disabled\".", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
5411
+ const result = await client.get(`/projects/${project_id}/webhooks/subscriptions`);
5412
+ return { content: [{
5413
+ type: "text",
5414
+ text: JSON.stringify(result, null, 2)
5415
+ }] };
5416
+ });
5417
+ registerTool(server, apiClient, "amba_webhooks_get", "Describe a single webhook subscription by id. Returns event_name, target_url, status, attempt metadata, and timestamps (no secret).", {
5418
+ project_id: z.string().describe("The project ID"),
5419
+ subscription_id: z.string().describe("The subscription UUID")
5420
+ }, async ({ project_id, subscription_id }, { client }) => {
5421
+ const result = await client.get(`/projects/${project_id}/webhooks/subscriptions/${subscription_id}`);
5422
+ return { content: [{
5423
+ type: "text",
5424
+ text: JSON.stringify(result, null, 2)
5425
+ }] };
5426
+ });
5427
+ registerTool(server, apiClient, "amba_webhooks_update", "Update a webhook subscription. Any of event_name, target_url, or status may be changed; supply at least one. status accepts only \"active\" or \"disabled\" — \"degraded\" is reserved for the automatic failure circuit breaker and cannot be set manually.", {
5428
+ project_id: z.string().describe("The project ID"),
5429
+ subscription_id: z.string().describe("The subscription UUID"),
5430
+ event_name: z.string().optional().describe("New event name. Must match ^[a-z][a-z0-9_.-]{0,127}$ (case-insensitive)."),
5431
+ target_url: z.string().optional().describe("New HTTPS endpoint. http:// allowed only for localhost."),
5432
+ status: z.enum(["active", "disabled"]).optional().describe("Enable or disable delivery. \"degraded\" cannot be set manually.")
5433
+ }, async ({ project_id, subscription_id, event_name, target_url, status }, { client }) => {
5434
+ const body = {};
5435
+ if (event_name !== void 0) body.event_name = event_name;
5436
+ if (target_url !== void 0) body.target_url = target_url;
5437
+ if (status !== void 0) body.status = status;
5438
+ const result = await client.patch(`/projects/${project_id}/webhooks/subscriptions/${subscription_id}`, body);
5439
+ return { content: [{
5440
+ type: "text",
5441
+ text: JSON.stringify(result, null, 2)
5442
+ }] };
5443
+ });
5444
+ registerTool(server, apiClient, "amba_webhooks_delete", "Delete a webhook subscription. No further events are delivered to its target_url after this.", {
5445
+ project_id: z.string().describe("The project ID"),
5446
+ subscription_id: z.string().describe("The subscription UUID")
5447
+ }, async ({ project_id, subscription_id }, { client }) => {
5448
+ const result = await client.delete(`/projects/${project_id}/webhooks/subscriptions/${subscription_id}`);
5449
+ return { content: [{
5450
+ type: "text",
5451
+ text: JSON.stringify(result, null, 2)
5452
+ }] };
5453
+ });
5454
+ registerTool(server, apiClient, "amba_webhooks_rotate_secret", "Rotate the HMAC signing secret for a subscription. The new secret is returned ONCE — update your receiver config with it. To avoid dropped deliveries, have your receiver accept both the old and new secret for a brief overlap window before retiring the old one.", {
5455
+ project_id: z.string().describe("The project ID"),
5456
+ subscription_id: z.string().describe("The subscription UUID")
5457
+ }, async ({ project_id, subscription_id }, { client }) => {
5458
+ const result = await client.post(`/projects/${project_id}/webhooks/subscriptions/${subscription_id}/rotate-secret`);
5459
+ return { content: [{
5460
+ type: "text",
5461
+ text: JSON.stringify(result, null, 2)
5462
+ }] };
5463
+ });
5464
+ registerTool(server, apiClient, "amba_webhooks_test", "Send a synthetic test delivery to a subscription to verify receiver setup. The test payload is tagged so it does not affect the failure circuit breaker or pollute your real event history. Returns a workflow id; poll amba_webhooks_deliveries_list shortly after for the result. Fails with 409 if the subscription is disabled.", {
5465
+ project_id: z.string().describe("The project ID"),
5466
+ subscription_id: z.string().describe("The subscription UUID")
5467
+ }, async ({ project_id, subscription_id }, { client }) => {
5468
+ const result = await client.post(`/projects/${project_id}/webhooks/subscriptions/${subscription_id}/test`);
5469
+ return { content: [{
5470
+ type: "text",
5471
+ text: JSON.stringify(result, null, 2)
5472
+ }] };
5473
+ });
5474
+ registerTool(server, apiClient, "amba_webhooks_deliveries_list", "List recent webhook delivery attempts (most recent first). Optionally filter by subscription_id and/or status. status is \"pending\", \"delivered\", \"failed\", or \"dlq\" (dead-lettered after exhausting retries). Default page size 50, max 200.", {
5475
+ project_id: z.string().describe("The project ID"),
5476
+ subscription_id: z.string().optional().describe("Filter to deliveries for a single subscription UUID."),
5477
+ status: z.enum([
5478
+ "pending",
5479
+ "delivered",
5480
+ "failed",
5481
+ "dlq"
5482
+ ]).optional().describe("Filter by delivery status."),
5483
+ limit: z.number().optional().describe("Page size (default 50, max 200).")
5484
+ }, async ({ project_id, subscription_id, status, limit }, { client }) => {
5485
+ const query = {};
5486
+ if (subscription_id !== void 0) query.subscription_id = subscription_id;
5487
+ if (status !== void 0) query.status = status;
5488
+ if (limit !== void 0) query.limit = String(limit);
5489
+ const result = await client.get(`/projects/${project_id}/webhooks/deliveries`, query);
5490
+ return { content: [{
5491
+ type: "text",
5492
+ text: JSON.stringify(result, null, 2)
5493
+ }] };
5494
+ });
5495
+ registerTool(server, apiClient, "amba_webhooks_deliveries_get", "Describe a single webhook delivery by id, including the full attempt log, attempt count, and last error.", {
5496
+ project_id: z.string().describe("The project ID"),
5497
+ delivery_id: z.string().describe("The delivery UUID")
5498
+ }, async ({ project_id, delivery_id }, { client }) => {
5499
+ const result = await client.get(`/projects/${project_id}/webhooks/deliveries/${delivery_id}`);
5500
+ return { content: [{
5501
+ type: "text",
5502
+ text: JSON.stringify(result, null, 2)
5503
+ }] };
5504
+ });
5505
+ registerTool(server, apiClient, "amba_webhooks_deliveries_replay", "Re-enqueue a terminated (failed or dead-lettered) delivery, rebuilding the original payload from the source event. The original idempotency key is preserved so receiver-side dedup treats it as a re-delivery, not a new event. Returns 409 if the delivery is still pending, or 410 if the source event has been purged.", {
5506
+ project_id: z.string().describe("The project ID"),
5507
+ delivery_id: z.string().describe("The delivery UUID to replay")
5508
+ }, async ({ project_id, delivery_id }, { client }) => {
5509
+ const result = await client.post(`/projects/${project_id}/webhooks/deliveries/${delivery_id}/replay`);
5510
+ return { content: [{
5511
+ type: "text",
5512
+ text: JSON.stringify(result, null, 2)
5513
+ }] };
5514
+ });
5515
+ }
5516
+ //#endregion
5517
+ //#region src/tools/email.ts
5518
+ function registerTools(server, apiClient) {
5519
+ registerTool(server, apiClient, "amba_email_templates_create", "Create or update a transactional email template (upsert keyed by name — re-creating an existing name overwrites it). Templates are referenced by name when sending. Subject and html_body are required; text_body is an optional plain-text fallback. name must match ^[a-z][a-z0-9_]*$.", {
5520
+ project_id: z.string().describe("The project ID"),
5521
+ name: z.string().describe("Template name. Must match ^[a-z][a-z0-9_]*$."),
5522
+ subject: z.string().describe("Email subject line. Supports template variables."),
5523
+ html_body: z.string().describe("HTML body. Supports template variables."),
5524
+ text_body: z.string().nullable().optional().describe("Optional plain-text fallback body. Pass null to leave unset.")
5525
+ }, async ({ project_id, name, subject, html_body, text_body }, { client }) => {
5526
+ const body = {
5527
+ name,
5528
+ subject,
5529
+ html_body
5530
+ };
5531
+ if (text_body !== void 0) body.text_body = text_body;
5532
+ const result = await client.post(`/projects/${project_id}/email/templates`, body);
5533
+ return { content: [{
5534
+ type: "text",
5535
+ text: JSON.stringify(result, null, 2)
5536
+ }] };
5537
+ });
5538
+ registerTool(server, apiClient, "amba_email_templates_list", "List all transactional email templates for a project, ordered by name.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
5539
+ const result = await client.get(`/projects/${project_id}/email/templates`);
5540
+ return { content: [{
5541
+ type: "text",
5542
+ text: JSON.stringify(result, null, 2)
5543
+ }] };
5544
+ });
5545
+ registerTool(server, apiClient, "amba_email_templates_get", "Get a single transactional email template by name, including its subject, html_body, and text_body.", {
5546
+ project_id: z.string().describe("The project ID"),
5547
+ name: z.string().describe("The template name")
5548
+ }, async ({ project_id, name }, { client }) => {
5549
+ const result = await client.get(`/projects/${project_id}/email/templates/${encodeURIComponent(name)}`);
5550
+ return { content: [{
5551
+ type: "text",
5552
+ text: JSON.stringify(result, null, 2)
5553
+ }] };
5554
+ });
5555
+ registerTool(server, apiClient, "amba_email_templates_update", "Update fields on an existing email template. Supply at least one of subject, html_body, or text_body. For text_body: pass a string to set it, or null to explicitly clear it; omit it to leave it unchanged.", {
5556
+ project_id: z.string().describe("The project ID"),
5557
+ name: z.string().describe("The template name to update"),
5558
+ subject: z.string().optional().describe("New subject line."),
5559
+ html_body: z.string().optional().describe("New HTML body."),
5560
+ text_body: z.string().nullable().optional().describe("New plain-text body; pass null to clear it, omit to leave unchanged.")
5561
+ }, async ({ project_id, name, subject, html_body, text_body }, { client }) => {
5562
+ const body = {};
5563
+ if (subject !== void 0) body.subject = subject;
5564
+ if (html_body !== void 0) body.html_body = html_body;
5565
+ if (text_body !== void 0) body.text_body = text_body;
5566
+ const result = await client.patch(`/projects/${project_id}/email/templates/${encodeURIComponent(name)}`, body);
5567
+ return { content: [{
5568
+ type: "text",
5569
+ text: JSON.stringify(result, null, 2)
5570
+ }] };
5571
+ });
5572
+ registerTool(server, apiClient, "amba_email_templates_delete", "Delete a transactional email template by name. Sends that reference a deleted template will fail with TEMPLATE_NOT_FOUND.", {
5573
+ project_id: z.string().describe("The project ID"),
5574
+ name: z.string().describe("The template name to delete")
5575
+ }, async ({ project_id, name }, { client }) => {
5576
+ const result = await client.delete(`/projects/${project_id}/email/templates/${encodeURIComponent(name)}`);
5577
+ return { content: [{
5578
+ type: "text",
5579
+ text: JSON.stringify(result, null, 2)
5580
+ }] };
5581
+ });
5582
+ registerTool(server, apiClient, "amba_email_suppressions_create", "Add an email address to the suppression list (upsert). Suppressed addresses are skipped on send. reason is \"bounce\", \"complaint\", or \"manual\" (default). Use this to honor unsubscribe or block requests.", {
5583
+ project_id: z.string().describe("The project ID"),
5584
+ email: z.string().describe("Email address to suppress (must contain @)."),
5585
+ reason: z.enum([
5586
+ "bounce",
5587
+ "complaint",
5588
+ "manual"
5589
+ ]).optional().describe("Why the address is suppressed. Defaults to \"manual\".")
5590
+ }, async ({ project_id, email, reason }, { client }) => {
5591
+ const body = { email };
5592
+ if (reason !== void 0) body.reason = reason;
5593
+ const result = await client.post(`/projects/${project_id}/email/suppressions`, body);
5594
+ return { content: [{
5595
+ type: "text",
5596
+ text: JSON.stringify(result, null, 2)
5597
+ }] };
5598
+ });
5599
+ registerTool(server, apiClient, "amba_email_suppressions_list", "List suppressed email addresses (most recent first, up to 500). Optionally filter by reason (\"bounce\", \"complaint\", or \"manual\").", {
5600
+ project_id: z.string().describe("The project ID"),
5601
+ reason: z.enum([
5602
+ "bounce",
5603
+ "complaint",
5604
+ "manual"
5605
+ ]).optional().describe("Filter to a single suppression reason.")
5606
+ }, async ({ project_id, reason }, { client }) => {
5607
+ const query = {};
5608
+ if (reason !== void 0) query.reason = reason;
5609
+ const result = await client.get(`/projects/${project_id}/email/suppressions`, query);
5610
+ return { content: [{
5611
+ type: "text",
5612
+ text: JSON.stringify(result, null, 2)
5613
+ }] };
5614
+ });
5615
+ registerTool(server, apiClient, "amba_email_suppressions_delete", "Remove an email address from the suppression list (operator unblock). Subsequent sends to this address are no longer skipped.", {
5616
+ project_id: z.string().describe("The project ID"),
5617
+ email: z.string().describe("Email address to unsuppress.")
5618
+ }, async ({ project_id, email }, { client }) => {
5619
+ const result = await client.delete(`/projects/${project_id}/email/suppressions/${encodeURIComponent(email)}`);
5620
+ return { content: [{
5621
+ type: "text",
5622
+ text: JSON.stringify(result, null, 2)
5623
+ }] };
5624
+ });
5625
+ registerTool(server, apiClient, "amba_email_send", "Send a transactional email. Recipient is either a user (to.user_id — resolves the user's email) or a raw address (to.email). Send via a template (template + optional data variables) OR ad-hoc to a raw address (subject + html/text). Suppressed recipients are skipped and returned with status \"suppressed\".", {
5626
+ project_id: z.string().describe("The project ID"),
5627
+ to: z.object({
5628
+ user_id: z.string().optional().describe("App user UUID; resolves the user's email server-side."),
5629
+ email: z.string().optional().describe("Raw recipient email address.")
5630
+ }).describe("Recipient. Provide exactly one of user_id or email."),
5631
+ template: z.string().optional().describe("Template name to render. Mutually exclusive with subject/html/text."),
5632
+ data: z.record(z.unknown()).optional().describe("Template variable map (used with `template`)."),
5633
+ subject: z.string().optional().describe("Ad-hoc subject. Requires to.email; mutually exclusive with `template`."),
5634
+ html: z.string().optional().describe("Ad-hoc HTML body (ad-hoc send only)."),
5635
+ text: z.string().optional().describe("Ad-hoc plain-text body (ad-hoc send only).")
5636
+ }, async ({ project_id, to, template, data, subject, html, text }, { client }) => {
5637
+ const body = { to };
5638
+ if (template !== void 0) body.template = template;
5639
+ if (data !== void 0) body.data = data;
5640
+ if (subject !== void 0) body.subject = subject;
5641
+ if (html !== void 0) body.html = html;
5642
+ if (text !== void 0) body.text = text;
5643
+ const result = await client.post(`/projects/${project_id}/email/send`, body);
5644
+ return { content: [{
5645
+ type: "text",
5646
+ text: JSON.stringify(result, null, 2)
5647
+ }] };
5648
+ });
5649
+ registerTool(server, apiClient, "amba_email_deliveries_list", "List recent email delivery records (most recent first). Optionally filter by template name and/or status (\"queued\", \"sent\", \"suppressed\", \"failed\"). Default page size 50, max 200.", {
5650
+ project_id: z.string().describe("The project ID"),
5651
+ template: z.string().optional().describe("Filter to a single template name."),
5652
+ status: z.enum([
5653
+ "queued",
5654
+ "sent",
5655
+ "suppressed",
5656
+ "failed"
5657
+ ]).optional().describe("Filter by delivery status."),
5658
+ limit: z.number().optional().describe("Page size (default 50, max 200).")
5659
+ }, async ({ project_id, template, status, limit }, { client }) => {
5660
+ const query = {};
5661
+ if (template !== void 0) query.template = template;
5662
+ if (status !== void 0) query.status = status;
5663
+ if (limit !== void 0) query.limit = String(limit);
5664
+ const result = await client.get(`/projects/${project_id}/email/deliveries`, query);
5665
+ return { content: [{
5666
+ type: "text",
5667
+ text: JSON.stringify(result, null, 2)
5668
+ }] };
5669
+ });
5670
+ registerTool(server, apiClient, "amba_email_deliveries_get", "Describe a single email delivery by id, including recipient, template, status, and last error if it failed.", {
5671
+ project_id: z.string().describe("The project ID"),
5672
+ delivery_id: z.string().describe("The delivery UUID")
5673
+ }, async ({ project_id, delivery_id }, { client }) => {
5674
+ const result = await client.get(`/projects/${project_id}/email/deliveries/${delivery_id}`);
5675
+ return { content: [{
5676
+ type: "text",
5677
+ text: JSON.stringify(result, null, 2)
5678
+ }] };
5679
+ });
4790
5680
  }
4791
5681
  //#endregion
4792
5682
  //#region src/resources/amba-setup-identity.ts
@@ -5586,6 +6476,7 @@ Real-money purchases need a billing integration configured separately — see \`
5586
6476
  | \`amba_currencies_update\` | Edit (rename, change caps, change auto-recharge). | \`{ project_id, currency_id, max_balance: 10000 }\` |
5587
6477
  | \`amba_currencies_delete\` | Delete a currency (irreversible — users lose their balance). | \`{ project_id, currency_id }\` |
5588
6478
  | \`amba_currencies_grant\` | Grant currency to a specific user. | \`{ project_id, app_user_id, currency_code: "gems", amount: 100, reason: "welcome_bonus" }\` |
6479
+ | \`amba_currencies_spend\` | Debit currency from a specific user. Atomic; returns INSUFFICIENT_FUNDS if balance is too low (no partial debit). | \`{ project_id, app_user_id, currency_code: "gems", amount: 5, reason: "ai_generation" }\` |
5589
6480
  | \`amba_currencies_get_transactions\` | Per-user transaction ledger. | \`{ project_id, user_id, currency_code: "gems", limit: 100 }\` |
5590
6481
 
5591
6482
  #### Currency grant rules (auto-grants)
@@ -5871,6 +6762,9 @@ Mostly SDK-side — admin tools are for moderation + stats.
5871
6762
 
5872
6763
  | Tool | Purpose | Example args |
5873
6764
  | --- | --- | --- |
6765
+ | \`amba_messaging_create_conversation\` | Create a conversation server-side (zero or more initial participants — for matchmaker-built circles). | \`{ project_id, type: "group", name: "Circle" }\` |
6766
+ | \`amba_messaging_add_participant\` | Add a user to a conversation (idempotent). The matchmaker primitive for growing membership over time. | \`{ project_id, conversation_id, user_id }\` |
6767
+ | \`amba_messaging_remove_participant\` | Remove a user from a conversation (idempotent). | \`{ project_id, conversation_id, user_id }\` |
5874
6768
  | \`amba_messaging_list_conversations\` | List conversations (admin / moderation view). | \`{ project_id, limit: 50 }\` |
5875
6769
  | \`amba_messaging_list_messages\` | List messages in a conversation. | \`{ project_id, conversation_id, limit: 100 }\` |
5876
6770
  | \`amba_messaging_delete_message\` | Hard-delete a message (moderation). | \`{ project_id, conversation_id, message_id }\` |
@@ -6092,23 +6986,26 @@ If gamification, economy, and social are the playable surface, **infrastructure
6092
6986
 
6093
6987
  A collection is a schema-first table inside the project's isolated tenant database. You describe the columns, the server creates the table and any indexes. Rows are scoped to the signed-in \`app_user\` automatically (server-enforced auto row-level isolation) for SDK clients — admin tools bypass this.
6094
6988
 
6989
+ Admin tools authenticate the developer/agent (pass \`pat\` or send it as the inbound Bearer) and take \`project_id\`. Client tools authenticate an end-user and take \`api_key\` (+ \`session_token\`) — NOT \`project_id\` and NOT a \`pat\`. Every row tool names the collection with \`name\`, never \`collection\`.
6990
+
6095
6991
  | Tool | Purpose | Example args |
6096
6992
  | --- | --- | --- |
6097
- | \`amba_collections_create\` | Create a typed collection. | \`{ project_id, name: "todos", columns: [{ name: "title", type: "text", nullable: false }, { name: "done", type: "boolean", nullable: false, default: false }, { name: "due_at", type: "timestamptz", nullable: true }] }\` |
6993
+ | \`amba_collections_create\` | Create a typed collection. Pass \`shared: true\` for developer-seeded GLOBAL content (question banks, lookup tables) so \`user_id\` is nullable. | \`{ project_id, name: "todos", columns: [{ name: "title", type: "text", nullable: false }, { name: "done", type: "boolean", nullable: false, default: false }, { name: "due_at", type: "timestamptz", nullable: true }], shared: false }\` |
6098
6994
  | \`amba_collections_list\` | List collections in this project. | \`{ project_id }\` |
6099
- | \`amba_collections_get\` | Read one collection's schema. | \`{ project_id, collection_name: "todos" }\` |
6100
- | \`amba_collections_alter\` | Add / drop columns, add / drop indexes. | \`{ project_id, collection_name: "todos", add_columns: [{ name: "priority", type: "int", nullable: true }] }\` |
6101
- | \`amba_collections_delete\` | Drop the table (destructive). | \`{ project_id, collection_name }\` |
6102
- | \`amba_admin_insert_row\` | Insert a row as the developer (bypasses user-scope). | \`{ project_id, collection: "todos", row: { title: "Sample", done: false } }\` |
6103
- | \`amba_admin_list_rows\` | Read rows as the developer. | \`{ project_id, collection: "todos", limit: 100 }\` |
6104
- | \`amba_client_insert_row\` | Insert as an end-user. Requires \`api_key\` + \`session_token\`. | \`{ project_id, api_key, session_token, collection: "todos", row: {...} }\` |
6105
- | \`amba_client_list_rows\` | Read as an end-user (auto user-scoped). | \`{ project_id, api_key, session_token, collection: "todos" }\` |
6106
- | \`amba_client_get_row\` | Get one row by id (end-user). | \`{ project_id, api_key, session_token, collection, row_id }\` |
6107
- | \`amba_client_update_row\` | Update one row (end-user). | \`{ project_id, api_key, session_token, collection, row_id, patch: {...} }\` |
6108
- | \`amba_client_delete_row\` | Delete one row (end-user). | \`{ project_id, api_key, session_token, collection, row_id }\` |
6109
- | \`amba_client_count_rows\` | Count rows matching a filter. | \`{ project_id, api_key, session_token, collection, filter: {...} }\` |
6110
- | \`amba_client_find_rows\` | Filter / sort / paginate rows. | \`{ project_id, api_key, session_token, collection, filter: {...}, order_by: [...], limit: 50 }\` |
6111
- | \`amba_client_find_nearest_rows\` | Vector-similarity search (rows with a \`vector(<dim>)\` column). | \`{ project_id, api_key, session_token, collection, vector_column: "embedding", query_vector: [...], k: 10 }\` |
6995
+ | \`amba_collections_get\` | Read one collection's schema. | \`{ project_id, name: "todos" }\` |
6996
+ | \`amba_collections_alter\` | Exactly ONE of: \`add_column\`, \`add_index\`, \`drop_column\`, or \`relax_user_id\` per call. \`relax_user_id: true\` converts an existing collection to shared (drops the \`user_id\` NOT NULL). | \`{ project_id, name: "todos", add_column: { name: "priority", type: "int", nullable: true } }\` |
6997
+ | \`amba_collections_delete\` | Drop the table (destructive). \`confirm\` must equal the collection name. | \`{ project_id, name: "todos", confirm: "todos" }\` |
6998
+ | \`amba_admin_insert_row\` | Insert one row as the developer (bypasses user-scope; \`user_id\` honored if present). | \`{ project_id, name: "todos", row: { title: "Sample", done: false } }\` |
6999
+ | \`amba_admin_insert_rows\` | Bulk-insert up to 500 rows in one atomic statement — the canonical seeding/migration path. \`on_conflict\`: \`"error"\` (default) or \`"skip"\`. | \`{ project_id, name: "questions", rows: [{ q: "..." }, { q: "..." }], on_conflict: "skip" }\` |
7000
+ | \`amba_admin_list_rows\` | Read rows as the developer. | \`{ project_id, name: "todos", limit: 100 }\` |
7001
+ | \`amba_client_insert_row\` | Insert as an end-user. Requires \`api_key\` (+ \`session_token\`). | \`{ api_key, session_token, name: "todos", row: {...} }\` |
7002
+ | \`amba_client_list_rows\` | Read as an end-user (auto user-scoped). | \`{ api_key, session_token, name: "todos" }\` |
7003
+ | \`amba_client_get_row\` | Get one row by id (end-user). | \`{ api_key, session_token, name: "todos", id }\` |
7004
+ | \`amba_client_update_row\` | Update one row by id (end-user). Fields go in \`set\`. Omit \`id\` + pass \`where\` for a bulk update. | \`{ api_key, session_token, name: "todos", id, set: {...} }\` |
7005
+ | \`amba_client_delete_row\` | Soft-delete one row by id (end-user). | \`{ api_key, session_token, name: "todos", id }\` |
7006
+ | \`amba_client_count_rows\` | Count rows matching an optional \`where\`. | \`{ api_key, session_token, name: "todos", where: {...} }\` |
7007
+ | \`amba_client_find_rows\` | Filter / sort / paginate rows (SDK-shaped \`filter\`). | \`{ api_key, session_token, name: "todos", filter: {...}, order: ["created_at desc"], limit: 50 }\` |
7008
+ | \`amba_client_find_nearest_rows\` | Vector-similarity search (rows with a \`vector(<dim>)\` column). | \`{ api_key, session_token, name: "todos", column: "embedding", to_vector: [...], k: 10 }\` |
6112
7009
 
6113
7010
  Column types: \`text\`, \`int\`, \`bigint\`, \`float\`, \`boolean\`, \`timestamptz\`, \`date\`, \`json\`, \`jsonb\`, \`uuid\`, \`vector(<dim>)\` (e.g. \`vector(1536)\` for OpenAI embeddings).
6114
7011
 
@@ -6623,6 +7520,8 @@ const TOOL_CATEGORY = {
6623
7520
  amba_list_xp_rules: "gamification",
6624
7521
  amba_xp_update_rule: "gamification",
6625
7522
  amba_xp_delete_rule: "gamification",
7523
+ amba_xp_grant: "gamification",
7524
+ amba_xp_deduct: "gamification",
6626
7525
  amba_xp_list_users: "gamification",
6627
7526
  amba_users_get_xp: "gamification",
6628
7527
  amba_get_user_xp: "gamification",
@@ -6636,11 +7535,14 @@ const TOOL_CATEGORY = {
6636
7535
  amba_update_achievement: "gamification",
6637
7536
  amba_achievements_delete: "gamification",
6638
7537
  amba_delete_achievement: "gamification",
7538
+ amba_achievements_grant: "gamification",
7539
+ amba_achievements_revoke: "gamification",
6639
7540
  amba_streaks_create: "gamification",
6640
7541
  amba_create_streak: "gamification",
6641
7542
  amba_streaks_list: "gamification",
6642
7543
  amba_streaks_update: "gamification",
6643
7544
  amba_streaks_delete: "gamification",
7545
+ amba_streaks_reset: "gamification",
6644
7546
  amba_leaderboards_create: "gamification",
6645
7547
  amba_create_leaderboard: "gamification",
6646
7548
  amba_leaderboards_list: "gamification",
@@ -6675,6 +7577,7 @@ const TOOL_CATEGORY = {
6675
7577
  amba_delete_currency: "economy",
6676
7578
  amba_currencies_grant: "economy",
6677
7579
  amba_grant_currency: "economy",
7580
+ amba_currencies_spend: "economy",
6678
7581
  amba_currencies_get_transactions: "economy",
6679
7582
  amba_get_currency_transactions: "economy",
6680
7583
  amba_currency_grant_rules_create: "economy",
@@ -6709,6 +7612,7 @@ const TOOL_CATEGORY = {
6709
7612
  amba_stores_patch_listing: "economy",
6710
7613
  amba_stores_delete_listing: "economy",
6711
7614
  amba_inventory_grant_item: "economy",
7615
+ amba_inventory_revoke_item: "economy",
6712
7616
  amba_grant_item: "economy",
6713
7617
  amba_users_get_inventory: "economy",
6714
7618
  amba_get_user_inventory: "economy",
@@ -6732,6 +7636,9 @@ const TOOL_CATEGORY = {
6732
7636
  amba_update_group_member: "social",
6733
7637
  amba_groups_remove_member: "social",
6734
7638
  amba_remove_group_member: "social",
7639
+ amba_messaging_create_conversation: "social",
7640
+ amba_messaging_add_participant: "social",
7641
+ amba_messaging_remove_participant: "social",
6735
7642
  amba_messaging_list_conversations: "social",
6736
7643
  amba_messaging_list_messages: "social",
6737
7644
  amba_messaging_delete_message: "social",
@@ -6770,6 +7677,12 @@ const TOOL_CATEGORY = {
6770
7677
  amba_get_analytics: "analytics",
6771
7678
  amba_sessions_list: "analytics",
6772
7679
  amba_sessions_analytics: "analytics",
7680
+ amba_funnels_list: "analytics",
7681
+ amba_funnels_create: "analytics",
7682
+ amba_funnels_get: "analytics",
7683
+ amba_funnels_update: "analytics",
7684
+ amba_funnels_delete: "analytics",
7685
+ amba_funnels_query: "analytics",
6773
7686
  amba_projects_create: "infrastructure",
6774
7687
  amba_create_project: "infrastructure",
6775
7688
  amba_projects_list: "infrastructure",
@@ -6836,6 +7749,10 @@ const TOOL_CATEGORY = {
6836
7749
  amba_remove_site_domain: "infrastructure",
6837
7750
  amba_sites_list_domains: "infrastructure",
6838
7751
  amba_list_site_domains: "infrastructure",
7752
+ amba_domains_search: "infrastructure",
7753
+ amba_domains_check: "infrastructure",
7754
+ amba_domains_purchase: "infrastructure",
7755
+ amba_domains_list: "infrastructure",
6839
7756
  amba_media_upload: "infrastructure",
6840
7757
  amba_upload_media: "infrastructure",
6841
7758
  amba_media_list: "infrastructure",
@@ -6886,7 +7803,34 @@ const TOOL_CATEGORY = {
6886
7803
  amba_billing_set_ceiling: "infrastructure",
6887
7804
  amba_projects_invite_member: "infrastructure",
6888
7805
  amba_projects_list_members: "infrastructure",
6889
- amba_projects_remove_member: "infrastructure"
7806
+ amba_projects_remove_member: "infrastructure",
7807
+ amba_projects_transfer_owner: "infrastructure",
7808
+ amba_webhooks_create: "infrastructure",
7809
+ amba_webhooks_list: "infrastructure",
7810
+ amba_webhooks_get: "infrastructure",
7811
+ amba_webhooks_update: "infrastructure",
7812
+ amba_webhooks_delete: "infrastructure",
7813
+ amba_webhooks_rotate_secret: "infrastructure",
7814
+ amba_webhooks_test: "infrastructure",
7815
+ amba_webhooks_deliveries_list: "infrastructure",
7816
+ amba_webhooks_deliveries_get: "infrastructure",
7817
+ amba_webhooks_deliveries_replay: "infrastructure",
7818
+ amba_email_templates_create: "engagement",
7819
+ amba_email_templates_list: "engagement",
7820
+ amba_email_templates_get: "engagement",
7821
+ amba_email_templates_update: "engagement",
7822
+ amba_email_templates_delete: "engagement",
7823
+ amba_email_suppressions_create: "engagement",
7824
+ amba_email_suppressions_list: "engagement",
7825
+ amba_email_suppressions_delete: "engagement",
7826
+ amba_email_send: "engagement",
7827
+ amba_email_deliveries_list: "engagement",
7828
+ amba_email_deliveries_get: "engagement",
7829
+ amba_admin_insert_rows: "infrastructure",
7830
+ amba_remote_configs_create: "infrastructure",
7831
+ amba_remote_configs_list: "infrastructure",
7832
+ amba_remote_configs_update: "infrastructure",
7833
+ amba_remote_configs_delete: "infrastructure"
6890
7834
  };
6891
7835
  /**
6892
7836
  * Look up the canonical category for an MCP tool name. Accepts both
@@ -6906,7 +7850,11 @@ function getToolCategory(toolName) {
6906
7850
  * called against it once.
6907
7851
  */
6908
7852
  function registerAllTools(server, apiClient) {
6909
- registerTools$7(server, apiClient);
7853
+ registerTools$10(server, apiClient);
7854
+ registerTools$29(server, apiClient);
7855
+ registerTools$28(server, apiClient);
7856
+ registerTools$27(server, apiClient);
7857
+ registerTools$26(server, apiClient);
6910
7858
  registerTools$25(server, apiClient);
6911
7859
  registerTools$24(server, apiClient);
6912
7860
  registerTools$23(server, apiClient);
@@ -6922,9 +7870,9 @@ function registerAllTools(server, apiClient) {
6922
7870
  registerTools$13(server, apiClient);
6923
7871
  registerTools$12(server, apiClient);
6924
7872
  registerTools$11(server, apiClient);
6925
- registerTools$10(server, apiClient);
6926
7873
  registerTools$9(server, apiClient);
6927
7874
  registerTools$8(server, apiClient);
7875
+ registerTools$7(server, apiClient);
6928
7876
  registerTools$6(server, apiClient);
6929
7877
  registerTools$5(server, apiClient);
6930
7878
  registerTools$4(server, apiClient);