@layers/amba-mcp 4.0.2 → 4.0.4
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/expo-build-prompt.js +11 -3
- package/dist/index.js +1553 -176
- package/dist/lib/annotations.d.ts +113 -0
- package/dist/resources/amba-setup-economy.d.ts +1 -1
- package/dist/resources/amba-setup-infrastructure.d.ts +1 -1
- package/dist/resources/amba-setup-social.d.ts +1 -1
- package/dist/resources/amba-setup.d.ts +1 -1
- package/dist/resources/index.d.ts +3 -3
- package/dist/tools/ai-prompts-admin.d.ts +5 -3
- package/dist/tools/ai-providers-admin.d.ts +30 -0
- package/dist/tools/domains.d.ts +33 -0
- package/dist/tools/email.d.ts +26 -0
- package/dist/tools/experiments.d.ts +24 -0
- package/dist/tools/funnels.d.ts +3 -0
- package/dist/tools/invites.d.ts +5 -4
- package/dist/tools/leagues.d.ts +12 -0
- package/dist/tools/promotion.d.ts +28 -0
- package/dist/tools/secrets.d.ts +8 -8
- package/dist/tools/webhooks.d.ts +27 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { INTEGRATION_PROVIDERS } from "@layers/amba-shared";
|
|
6
|
+
import { INTEGRATION_PROVIDERS, integrationConfigSummary } from "@layers/amba-shared";
|
|
7
7
|
//#region src/auth.ts
|
|
8
8
|
const CREDENTIALS_PATH = join(homedir(), ".amba", "credentials.json");
|
|
9
9
|
async function loadCredentials() {
|
|
@@ -286,6 +286,236 @@ 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
|
+
"aggregate",
|
|
313
|
+
"results"
|
|
314
|
+
]);
|
|
315
|
+
/**
|
|
316
|
+
* Destructive verbs. A tool whose name contains one of these MAY perform
|
|
317
|
+
* a destructive update (irreversible removal of existing state). Maps to
|
|
318
|
+
* `destructiveHint: true`. `reset` is included because `*_reset_sandbox`
|
|
319
|
+
* wipes end-user data.
|
|
320
|
+
*/
|
|
321
|
+
const DESTRUCTIVE_VERBS = new Set([
|
|
322
|
+
"delete",
|
|
323
|
+
"remove",
|
|
324
|
+
"revoke",
|
|
325
|
+
"drop",
|
|
326
|
+
"reset"
|
|
327
|
+
]);
|
|
328
|
+
/**
|
|
329
|
+
* Write verbs — mutations that are additive / non-destructive (create a
|
|
330
|
+
* row, update fields, grant currency, deploy a function, send a push, …).
|
|
331
|
+
* Maps to `destructiveHint: false`. This set exists so a write tool that
|
|
332
|
+
* matches NONE of the read/destructive verbs is still positively
|
|
333
|
+
* classified (rather than falling through unannotated). Kept explicit so
|
|
334
|
+
* an unrecognised verb surfaces as a test failure instead of silently
|
|
335
|
+
* defaulting.
|
|
336
|
+
*/
|
|
337
|
+
const WRITE_VERBS = new Set([
|
|
338
|
+
"create",
|
|
339
|
+
"update",
|
|
340
|
+
"set",
|
|
341
|
+
"patch",
|
|
342
|
+
"grant",
|
|
343
|
+
"spend",
|
|
344
|
+
"deduct",
|
|
345
|
+
"deploy",
|
|
346
|
+
"send",
|
|
347
|
+
"add",
|
|
348
|
+
"configure",
|
|
349
|
+
"invoke",
|
|
350
|
+
"schedule",
|
|
351
|
+
"assign",
|
|
352
|
+
"upload",
|
|
353
|
+
"rotate",
|
|
354
|
+
"reprovision",
|
|
355
|
+
"transfer",
|
|
356
|
+
"pause",
|
|
357
|
+
"resume",
|
|
358
|
+
"trigger",
|
|
359
|
+
"evaluate",
|
|
360
|
+
"test",
|
|
361
|
+
"approve",
|
|
362
|
+
"reject",
|
|
363
|
+
"escalate",
|
|
364
|
+
"replay",
|
|
365
|
+
"import",
|
|
366
|
+
"alter",
|
|
367
|
+
"insert",
|
|
368
|
+
"invite",
|
|
369
|
+
"register",
|
|
370
|
+
"purchase",
|
|
371
|
+
"signup",
|
|
372
|
+
"login",
|
|
373
|
+
"refresh"
|
|
374
|
+
]);
|
|
375
|
+
/**
|
|
376
|
+
* Split a tool name into lowercase verb-candidate tokens, dropping the
|
|
377
|
+
* `amba_` prefix. Exported for the test suite.
|
|
378
|
+
*/
|
|
379
|
+
function toolNameTokens(name) {
|
|
380
|
+
return name.replace(/^amba_/, "").split("_").filter(Boolean);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Classify a tool name into its behavioural class by PRECEDENCE
|
|
384
|
+
* (destructive > read > write). Returns `null` if no token matches any
|
|
385
|
+
* known verb — that should never happen for a real Amba tool and the
|
|
386
|
+
* test suite asserts it doesn't, so a `null` at runtime means a new tool
|
|
387
|
+
* used an unrecognised verb and needs a verb-set entry here.
|
|
388
|
+
*/
|
|
389
|
+
function classifyToolVerb(name) {
|
|
390
|
+
const tokens = toolNameTokens(name);
|
|
391
|
+
let sawRead = false;
|
|
392
|
+
let sawWrite = false;
|
|
393
|
+
for (const token of tokens) {
|
|
394
|
+
if (DESTRUCTIVE_VERBS.has(token)) return "destructive";
|
|
395
|
+
if (READ_VERBS.has(token)) sawRead = true;
|
|
396
|
+
else if (WRITE_VERBS.has(token)) sawWrite = true;
|
|
397
|
+
}
|
|
398
|
+
if (sawRead) return "read";
|
|
399
|
+
if (sawWrite) return "write";
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
/** Title-case a single token (`currencies` → `Currencies`). */
|
|
403
|
+
function capitalize(token) {
|
|
404
|
+
return token.length === 0 ? token : token[0].toUpperCase() + token.slice(1);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Verb-set tokens that read as nouns/objects far more often than as the
|
|
408
|
+
* operation verb, so they must NOT be chosen as the title's leading verb.
|
|
409
|
+
* They stay in the verb sets above for CLASSIFICATION (e.g. `*_status`,
|
|
410
|
+
* `*_stats` are read-only), but for TITLE purposes they're objects:
|
|
411
|
+
*
|
|
412
|
+
* amba_get_friendship_stats → "Get Friendship Stats" (not "Stats …")
|
|
413
|
+
* amba_pause_function_schedule→ "Pause Function Schedule" (not "Schedule …")
|
|
414
|
+
* amba_functions_get_logs → "Get Functions Logs"
|
|
415
|
+
* amba_billing_status → "Billing Status" (no action verb at all)
|
|
416
|
+
* amba_sessions_analytics → "Sessions Analytics"
|
|
417
|
+
*/
|
|
418
|
+
const NOUN_LIKE_VERBS = new Set([
|
|
419
|
+
"stats",
|
|
420
|
+
"status",
|
|
421
|
+
"logs",
|
|
422
|
+
"tiers",
|
|
423
|
+
"analytics",
|
|
424
|
+
"schedule",
|
|
425
|
+
"me"
|
|
426
|
+
]);
|
|
427
|
+
/**
|
|
428
|
+
* The verbs eligible to be a title's leading word: every classification
|
|
429
|
+
* verb that isn't a noun-like token. These are unambiguous action verbs
|
|
430
|
+
* (`create`, `get`, `delete`, `set`, `pause`, …).
|
|
431
|
+
*/
|
|
432
|
+
const STRONG_TITLE_VERBS = new Set([
|
|
433
|
+
...READ_VERBS,
|
|
434
|
+
...WRITE_VERBS,
|
|
435
|
+
...DESTRUCTIVE_VERBS
|
|
436
|
+
].filter((v) => !NOUN_LIKE_VERBS.has(v)));
|
|
437
|
+
/**
|
|
438
|
+
* Pick the index of the OPERATIVE action verb for the title, or -1 if the
|
|
439
|
+
* name has no action verb (e.g. `billing_status`, `sessions_analytics`).
|
|
440
|
+
*
|
|
441
|
+
* A naive `tokens.find()` is wrong two different ways:
|
|
442
|
+
* - `amba_currency_grant_rules_create` has `grant` (a noun-modifier in
|
|
443
|
+
* `grant_rules`) AND `create`; `find` picks `grant` → "Grant … Create".
|
|
444
|
+
* - `amba_get_friendship_stats` has `get` AND `stats`; preferring the
|
|
445
|
+
* trailing match picks `stats` → "Stats Get Friendship".
|
|
446
|
+
*
|
|
447
|
+
* Amba's naming puts the operation verb at a token-list BOUNDARY: leading
|
|
448
|
+
* for legacy `amba_<verb>_<resource>`, trailing/mid for canonical
|
|
449
|
+
* `amba_<resource>_<verb>[_<object>]`. So:
|
|
450
|
+
* 1. If the FIRST token is a strong verb → it's the operative verb
|
|
451
|
+
* (legacy alias: `get_friendship_stats`, `create_currency`).
|
|
452
|
+
* 2. Else take the LAST strong verb — skips trailing object nouns like
|
|
453
|
+
* `stats`/`schedule` (excluded from `STRONG_TITLE_VERBS`) and skips
|
|
454
|
+
* mid-name noun-modifiers like `grant` in `grant_rules_create`
|
|
455
|
+
* (`get_stats`/`set_price`/`grant_rules_create` all resolve right).
|
|
456
|
+
* 3. No strong verb → -1 (caller title-cases in order).
|
|
457
|
+
*/
|
|
458
|
+
function operativeVerbIndex(tokens) {
|
|
459
|
+
if (tokens.length > 0 && STRONG_TITLE_VERBS.has(tokens[0])) return 0;
|
|
460
|
+
let lastStrong = -1;
|
|
461
|
+
for (let i = 0; i < tokens.length; i += 1) if (STRONG_TITLE_VERBS.has(tokens[i])) lastStrong = i;
|
|
462
|
+
return lastStrong;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Derive a human-readable Title-Case title from the tool name. The
|
|
466
|
+
* operative action verb (see `operativeVerbIndex`) is moved to the front
|
|
467
|
+
* so the title reads as an imperative; remaining tokens keep their order:
|
|
468
|
+
*
|
|
469
|
+
* amba_currencies_create → "Create Currencies"
|
|
470
|
+
* amba_content_delete_schedule → "Delete Content Schedule"
|
|
471
|
+
* amba_currency_grant_rules_create → "Create Currency Grant Rules"
|
|
472
|
+
* amba_get_friendship_stats → "Get Friendship Stats"
|
|
473
|
+
* amba_pause_function_schedule → "Pause Function Schedule"
|
|
474
|
+
* amba_catalog_items_set_price → "Set Catalog Items Price"
|
|
475
|
+
* amba_users_get → "Get Users"
|
|
476
|
+
*
|
|
477
|
+
* When there is no action verb (`billing_status`, `sessions_analytics`)
|
|
478
|
+
* the tokens are title-cased in order — a readable noun-phrase fallback.
|
|
479
|
+
*/
|
|
480
|
+
function deriveToolTitle(name) {
|
|
481
|
+
const tokens = toolNameTokens(name);
|
|
482
|
+
const idx = operativeVerbIndex(tokens);
|
|
483
|
+
if (idx >= 0) {
|
|
484
|
+
const rest = tokens.filter((_, i) => i !== idx);
|
|
485
|
+
return [tokens[idx], ...rest].map(capitalize).join(" ");
|
|
486
|
+
}
|
|
487
|
+
return tokens.map(capitalize).join(" ");
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Derive the full `ToolAnnotations` (title + read-only/destructive hints)
|
|
491
|
+
* for a tool name. This is the single function `with-pat.ts` calls for
|
|
492
|
+
* every tool it registers, so annotations stay uniform and automatic.
|
|
493
|
+
*/
|
|
494
|
+
function deriveToolAnnotations(name) {
|
|
495
|
+
const title = deriveToolTitle(name);
|
|
496
|
+
switch (classifyToolVerb(name)) {
|
|
497
|
+
case "read": return {
|
|
498
|
+
title,
|
|
499
|
+
readOnlyHint: true
|
|
500
|
+
};
|
|
501
|
+
case "destructive": return {
|
|
502
|
+
title,
|
|
503
|
+
readOnlyHint: false,
|
|
504
|
+
destructiveHint: true
|
|
505
|
+
};
|
|
506
|
+
case "write": return {
|
|
507
|
+
title,
|
|
508
|
+
readOnlyHint: false,
|
|
509
|
+
destructiveHint: false
|
|
510
|
+
};
|
|
511
|
+
default: return {
|
|
512
|
+
title,
|
|
513
|
+
readOnlyHint: false,
|
|
514
|
+
destructiveHint: true
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
//#endregion
|
|
289
519
|
//#region src/lib/with-pat.ts
|
|
290
520
|
/**
|
|
291
521
|
* The `pat` schema entry injected into every tool that goes through
|
|
@@ -362,8 +592,8 @@ function registerTool(server, apiClient, name, description, schema, handler, ali
|
|
|
362
592
|
client
|
|
363
593
|
});
|
|
364
594
|
};
|
|
365
|
-
server.tool(name, description, extendedSchema, wrappedHandler(name));
|
|
366
|
-
for (const alias of aliases) server.tool(alias, description, extendedSchema, (async (rawArgs) => {
|
|
595
|
+
server.tool(name, description, extendedSchema, deriveToolAnnotations(name), wrappedHandler(name));
|
|
596
|
+
for (const alias of aliases) server.tool(alias, description, extendedSchema, deriveToolAnnotations(alias), (async (rawArgs) => {
|
|
367
597
|
warnDeprecatedAlias(alias, name);
|
|
368
598
|
return wrappedHandler(name)(rawArgs);
|
|
369
599
|
}));
|
|
@@ -375,15 +605,15 @@ function registerTool(server, apiClient, name, description, schema, handler, ali
|
|
|
375
605
|
* should go through `registerTool`.
|
|
376
606
|
*/
|
|
377
607
|
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) => {
|
|
608
|
+
server.tool(name, description, schema, deriveToolAnnotations(name), handler);
|
|
609
|
+
for (const alias of aliases) server.tool(alias, description, schema, deriveToolAnnotations(alias), (async (args) => {
|
|
380
610
|
warnDeprecatedAlias(alias, name);
|
|
381
611
|
return handler(args);
|
|
382
612
|
}));
|
|
383
613
|
}
|
|
384
614
|
//#endregion
|
|
385
615
|
//#region src/tools/projects.ts
|
|
386
|
-
function registerTools$
|
|
616
|
+
function registerTools$33(server, apiClient) {
|
|
387
617
|
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
618
|
const result = await client.get("/projects");
|
|
389
619
|
return { content: [{
|
|
@@ -490,7 +720,7 @@ function registerTools$25(server, apiClient) {
|
|
|
490
720
|
}
|
|
491
721
|
//#endregion
|
|
492
722
|
//#region src/tools/push.ts
|
|
493
|
-
function registerTools$
|
|
723
|
+
function registerTools$32(server, apiClient) {
|
|
494
724
|
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
725
|
project_id: z.string().describe("The project ID"),
|
|
496
726
|
title: z.string().describe("Push notification title shown to the user"),
|
|
@@ -620,7 +850,7 @@ const segmentRulesSchema = z.object({
|
|
|
620
850
|
operator: z.enum(["AND", "OR"]).describe("Logical operator combining conditions"),
|
|
621
851
|
conditions: z.array(segmentConditionSchema).describe("Array of filter conditions")
|
|
622
852
|
});
|
|
623
|
-
function registerTools$
|
|
853
|
+
function registerTools$31(server, apiClient) {
|
|
624
854
|
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
855
|
project_id: z.string().describe("The project ID"),
|
|
626
856
|
include_system: z.boolean().optional().describe("Include built-in system segments in the result. Defaults to false.")
|
|
@@ -705,8 +935,8 @@ const configConditionSchema = z.object({
|
|
|
705
935
|
percentage: z.number().optional().describe("Percentage rollout (0-100)"),
|
|
706
936
|
value: z.unknown().describe("Override value for this condition")
|
|
707
937
|
});
|
|
708
|
-
function registerTools$
|
|
709
|
-
registerTool(server, apiClient, "
|
|
938
|
+
function registerTools$30(server, apiClient) {
|
|
939
|
+
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
940
|
project_id: z.string().describe("The project ID"),
|
|
711
941
|
include_system: z.boolean().optional().describe("Include platform-provided default config keys in the result. Defaults to false.")
|
|
712
942
|
}, async ({ project_id, include_system }, { client }) => {
|
|
@@ -717,8 +947,8 @@ function registerTools$22(server, apiClient) {
|
|
|
717
947
|
type: "text",
|
|
718
948
|
text: JSON.stringify(result, null, 2)
|
|
719
949
|
}] };
|
|
720
|
-
}, ["amba_list_configs"]);
|
|
721
|
-
registerTool(server, apiClient, "
|
|
950
|
+
}, ["amba_configs_list", "amba_list_configs"]);
|
|
951
|
+
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
952
|
project_id: z.string().describe("The project ID"),
|
|
723
953
|
key: z.string().describe("Config key name (e.g. \"feature_paywall_v2\", \"daily_limit\")"),
|
|
724
954
|
value: z.unknown().describe("The default config value"),
|
|
@@ -743,8 +973,8 @@ function registerTools$22(server, apiClient) {
|
|
|
743
973
|
type: "text",
|
|
744
974
|
text: JSON.stringify(result, null, 2)
|
|
745
975
|
}] };
|
|
746
|
-
}, ["amba_create_config"]);
|
|
747
|
-
registerTool(server, apiClient, "
|
|
976
|
+
}, ["amba_configs_create", "amba_create_config"]);
|
|
977
|
+
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
978
|
project_id: z.string().describe("The project ID"),
|
|
749
979
|
key: z.string().describe("The config key to update"),
|
|
750
980
|
value: z.unknown().optional().describe("New default value (omit to leave unchanged)"),
|
|
@@ -760,8 +990,8 @@ function registerTools$22(server, apiClient) {
|
|
|
760
990
|
type: "text",
|
|
761
991
|
text: JSON.stringify(result, null, 2)
|
|
762
992
|
}] };
|
|
763
|
-
}, ["amba_update_config"]);
|
|
764
|
-
registerTool(server, apiClient, "
|
|
993
|
+
}, ["amba_configs_update", "amba_update_config"]);
|
|
994
|
+
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
995
|
project_id: z.string().describe("The project ID"),
|
|
766
996
|
key: z.string().describe("The config key to delete")
|
|
767
997
|
}, async ({ project_id, key }, { client }) => {
|
|
@@ -770,7 +1000,7 @@ function registerTools$22(server, apiClient) {
|
|
|
770
1000
|
type: "text",
|
|
771
1001
|
text: JSON.stringify(result, null, 2)
|
|
772
1002
|
}] };
|
|
773
|
-
}, ["amba_delete_config"]);
|
|
1003
|
+
}, ["amba_configs_delete", "amba_delete_config"]);
|
|
774
1004
|
}
|
|
775
1005
|
//#endregion
|
|
776
1006
|
//#region src/tools/content.ts
|
|
@@ -783,8 +1013,8 @@ const contentItemSchema = z.object({
|
|
|
783
1013
|
metadata: z.record(z.unknown()).optional().describe("Arbitrary metadata key-value pairs"),
|
|
784
1014
|
is_premium: z.boolean().optional().describe("Whether this content requires a premium entitlement")
|
|
785
1015
|
});
|
|
786
|
-
function registerTools$
|
|
787
|
-
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.", {
|
|
1016
|
+
function registerTools$29(server, apiClient) {
|
|
1017
|
+
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. The library `name` IS the channel handle the in-app SDK uses: `Amba.content.library(name)` reads this library (names are unique). The response echoes it as `channel`.", {
|
|
788
1018
|
project_id: z.string().describe("The project ID"),
|
|
789
1019
|
name: z.string().describe("Library name (e.g. \"Daily Motivation\", \"Workout Tips\")"),
|
|
790
1020
|
description: z.string().optional().describe("Description of the library content"),
|
|
@@ -799,6 +1029,17 @@ function registerTools$21(server, apiClient) {
|
|
|
799
1029
|
text: JSON.stringify(result, null, 2)
|
|
800
1030
|
}] };
|
|
801
1031
|
}, ["amba_create_content_library"]);
|
|
1032
|
+
registerTool(server, apiClient, "amba_content_libraries_delete", "Delete a content library. A library with items is refused unless cascade=true, which deletes the library and ALL its items, schedules, and deliveries in one call (irreversible). Use this to clean up orphan libraries instead of deleting items one at a time.", {
|
|
1033
|
+
project_id: z.string().describe("The project ID"),
|
|
1034
|
+
library_id: z.string().describe("The content library ID to delete"),
|
|
1035
|
+
cascade: z.boolean().optional().describe("Required to delete a non-empty library. When true, deletes the library and all its items, schedules, and deliveries. Defaults to false (a non-empty library returns 409 LIBRARY_NOT_EMPTY).")
|
|
1036
|
+
}, async ({ project_id, library_id, cascade }, { client }) => {
|
|
1037
|
+
const result = await client.delete(`/projects/${project_id}/content/libraries/${library_id}`, cascade ? { cascade: "true" } : void 0);
|
|
1038
|
+
return { content: [{
|
|
1039
|
+
type: "text",
|
|
1040
|
+
text: JSON.stringify(result, null, 2)
|
|
1041
|
+
}] };
|
|
1042
|
+
});
|
|
802
1043
|
registerTool(server, apiClient, "amba_content_items_add", "Add one or more content items to an existing content library. Items can include text, media URLs, categories, tags, and metadata.", {
|
|
803
1044
|
project_id: z.string().describe("The project ID"),
|
|
804
1045
|
library_id: z.string().describe("The content library ID"),
|
|
@@ -842,7 +1083,7 @@ function registerTools$21(server, apiClient) {
|
|
|
842
1083
|
text: JSON.stringify(result, null, 2)
|
|
843
1084
|
}] };
|
|
844
1085
|
}, ["amba_create_content_schedule"]);
|
|
845
|
-
registerTool(server, apiClient, "amba_content_list_libraries", "List content libraries for a project.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
1086
|
+
registerTool(server, apiClient, "amba_content_list_libraries", "List content libraries for a project. Each library exposes `channel` (= name) — the handle the in-app SDK passes to `Amba.content.library(channel)` (and `Amba.content.today(scheduleName)` for scheduled rotations).", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
846
1087
|
const result = await client.get(`/projects/${project_id}/content/libraries`);
|
|
847
1088
|
return { content: [{
|
|
848
1089
|
type: "text",
|
|
@@ -947,7 +1188,7 @@ function registerTools$21(server, apiClient) {
|
|
|
947
1188
|
}
|
|
948
1189
|
//#endregion
|
|
949
1190
|
//#region src/tools/streaks.ts
|
|
950
|
-
function registerTools$
|
|
1191
|
+
function registerTools$28(server, apiClient) {
|
|
951
1192
|
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
1193
|
project_id: z.string().describe("The project ID"),
|
|
953
1194
|
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,15 +1262,29 @@ function registerTools$20(server, apiClient) {
|
|
|
1021
1262
|
text: JSON.stringify(result, null, 2)
|
|
1022
1263
|
}] };
|
|
1023
1264
|
});
|
|
1265
|
+
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.", {
|
|
1266
|
+
project_id: z.string().describe("The project ID"),
|
|
1267
|
+
streak_id: z.string().describe("The streak definition ID"),
|
|
1268
|
+
app_user_id: z.string().describe("The user whose streak to reset"),
|
|
1269
|
+
set_count: z.number().int().nonnegative().optional().describe("Restore the streak to this count. Omit to zero the streak (clean restart).")
|
|
1270
|
+
}, async ({ project_id, streak_id, app_user_id, set_count }, { client }) => {
|
|
1271
|
+
const payload = { app_user_id };
|
|
1272
|
+
if (set_count !== void 0) payload.set_count = set_count;
|
|
1273
|
+
const result = await client.post(`/projects/${project_id}/streaks/${streak_id}/reset`, payload);
|
|
1274
|
+
return { content: [{
|
|
1275
|
+
type: "text",
|
|
1276
|
+
text: JSON.stringify(result, null, 2)
|
|
1277
|
+
}] };
|
|
1278
|
+
});
|
|
1024
1279
|
}
|
|
1025
1280
|
//#endregion
|
|
1026
1281
|
//#region src/tools/integrations.ts
|
|
1027
1282
|
const providerEnum = z.enum(INTEGRATION_PROVIDERS);
|
|
1028
|
-
function registerTools$
|
|
1029
|
-
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.", {
|
|
1283
|
+
function registerTools$27(server, apiClient) {
|
|
1284
|
+
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. Push credentials (.p8 / service-account JSON) are passed inline here and stored server-side automatically — no separate secrets step.", {
|
|
1030
1285
|
project_id: z.string().describe("The project ID"),
|
|
1031
1286
|
provider: providerEnum.describe("Integration provider name"),
|
|
1032
|
-
config: z.record(z.unknown()).describe("Provider-specific configuration.
|
|
1287
|
+
config: z.record(z.unknown()).describe("Provider-specific configuration. " + integrationConfigSummary())
|
|
1033
1288
|
}, async ({ project_id, provider, config }, { client }) => {
|
|
1034
1289
|
const result = await client.post(`/projects/${project_id}/integrations`, {
|
|
1035
1290
|
provider,
|
|
@@ -1095,7 +1350,7 @@ const PERIOD_DAYS = {
|
|
|
1095
1350
|
"30d": "30",
|
|
1096
1351
|
"90d": "90"
|
|
1097
1352
|
};
|
|
1098
|
-
function registerTools$
|
|
1353
|
+
function registerTools$26(server, apiClient) {
|
|
1099
1354
|
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
1355
|
project_id: z.string().describe("The project ID"),
|
|
1101
1356
|
period: z.enum([
|
|
@@ -1118,7 +1373,7 @@ function registerTools$18(server, apiClient) {
|
|
|
1118
1373
|
}
|
|
1119
1374
|
//#endregion
|
|
1120
1375
|
//#region src/tools/users.ts
|
|
1121
|
-
function registerTools$
|
|
1376
|
+
function registerTools$25(server, apiClient) {
|
|
1122
1377
|
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
1378
|
project_id: z.string().describe("The project ID"),
|
|
1124
1379
|
limit: z.number().optional().describe("Maximum number of users to return (default 50)"),
|
|
@@ -1137,6 +1392,30 @@ function registerTools$17(server, apiClient) {
|
|
|
1137
1392
|
text: JSON.stringify(result, null, 2)
|
|
1138
1393
|
}] };
|
|
1139
1394
|
}, ["amba_list_users"]);
|
|
1395
|
+
registerTool(server, apiClient, "amba_users_create", "Admin-create an app user from the developer/agent side. Use this to mint a system/seed user to attribute migrated rows to — e.g. after amba_users_reset_sandbox wiped everyone. All fields are optional; with no fields it mints an anonymous user. Collisions on external_id/anonymous_id/email return 409 USER_ALREADY_EXISTS.", {
|
|
1396
|
+
project_id: z.string().describe("The project ID"),
|
|
1397
|
+
external_id: z.string().optional().describe("Your stable external identifier for this user (unique). Optional."),
|
|
1398
|
+
anonymous_id: z.string().optional().describe("Anonymous device id (unique). Optional; auto-generated if no identity given."),
|
|
1399
|
+
email: z.string().optional().describe("Email address. Optional."),
|
|
1400
|
+
phone: z.string().optional().describe("Phone number. Optional."),
|
|
1401
|
+
display_name: z.string().optional().describe("Display name. Optional."),
|
|
1402
|
+
avatar_url: z.string().optional().describe("Avatar URL. Optional."),
|
|
1403
|
+
properties: z.record(z.unknown()).optional().describe("Arbitrary user properties (JSON object). Optional.")
|
|
1404
|
+
}, async ({ project_id, external_id, anonymous_id, email, phone, display_name, avatar_url, properties }, { client }) => {
|
|
1405
|
+
const payload = {};
|
|
1406
|
+
if (external_id !== void 0) payload.external_id = external_id;
|
|
1407
|
+
if (anonymous_id !== void 0) payload.anonymous_id = anonymous_id;
|
|
1408
|
+
if (email !== void 0) payload.email = email;
|
|
1409
|
+
if (phone !== void 0) payload.phone = phone;
|
|
1410
|
+
if (display_name !== void 0) payload.display_name = display_name;
|
|
1411
|
+
if (avatar_url !== void 0) payload.avatar_url = avatar_url;
|
|
1412
|
+
if (properties !== void 0) payload.properties = properties;
|
|
1413
|
+
const result = await client.post(`/projects/${project_id}/users`, payload);
|
|
1414
|
+
return { content: [{
|
|
1415
|
+
type: "text",
|
|
1416
|
+
text: JSON.stringify(result, null, 2)
|
|
1417
|
+
}] };
|
|
1418
|
+
}, ["amba_create_user"]);
|
|
1140
1419
|
registerTool(server, apiClient, "amba_users_get", "Get a single app user by id, with their streaks and entitlements joined in.", {
|
|
1141
1420
|
project_id: z.string().describe("The project ID"),
|
|
1142
1421
|
user_id: z.string().describe("The app user ID")
|
|
@@ -1396,12 +1675,18 @@ println("Current streak: \${streak.currentCount}")`,
|
|
|
1396
1675
|
// For Expo apps, use @layers/amba-expo instead.
|
|
1397
1676
|
import { Amba } from '@layers/amba-react-native';
|
|
1398
1677
|
|
|
1399
|
-
// Configure once at app startup
|
|
1678
|
+
// Configure once at app startup. The first authenticated call lazily
|
|
1679
|
+
// establishes an anonymous session, so reads work without a manual sign-in.
|
|
1680
|
+
// Call signInAnonymously() explicitly if you want to control when the
|
|
1681
|
+
// anonymous user is created.
|
|
1400
1682
|
await Amba.configure({
|
|
1401
1683
|
projectId: 'YOUR_PROJECT_ID',
|
|
1402
|
-
|
|
1684
|
+
clientKey: 'YOUR_CLIENT_API_KEY',
|
|
1403
1685
|
});
|
|
1404
1686
|
|
|
1687
|
+
// Optional — establish the session up front instead of lazily:
|
|
1688
|
+
// await Amba.auth.signInAnonymously();
|
|
1689
|
+
|
|
1405
1690
|
// Track events
|
|
1406
1691
|
await Amba.events.track('workout_completed', {
|
|
1407
1692
|
duration_minutes: 30,
|
|
@@ -1423,9 +1708,11 @@ import { Amba } from '@layers/amba-expo';
|
|
|
1423
1708
|
|
|
1424
1709
|
export default function RootLayout() {
|
|
1425
1710
|
useEffect(() => {
|
|
1426
|
-
|
|
1711
|
+
// configure() is async. Reads lazily establish an anonymous session on the
|
|
1712
|
+
// first authenticated call, so no manual sign-in is required to start.
|
|
1713
|
+
void Amba.configure({
|
|
1427
1714
|
projectId: process.env.EXPO_PUBLIC_AMBA_PROJECT_ID!,
|
|
1428
|
-
|
|
1715
|
+
clientKey: 'YOUR_CLIENT_API_KEY',
|
|
1429
1716
|
});
|
|
1430
1717
|
}, []);
|
|
1431
1718
|
|
|
@@ -1491,7 +1778,7 @@ final dailyLimit = await Amba.config.get<int>('daily_limit', defaultValue: 10);
|
|
|
1491
1778
|
final streak = await Amba.streaks.get('daily_login');
|
|
1492
1779
|
print('Current streak: \${streak.currentCount}');`
|
|
1493
1780
|
};
|
|
1494
|
-
function registerTools$
|
|
1781
|
+
function registerTools$24(server, apiClient) {
|
|
1495
1782
|
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
1783
|
"ios",
|
|
1497
1784
|
"android",
|
|
@@ -1547,7 +1834,7 @@ function registerTools$16(server, apiClient) {
|
|
|
1547
1834
|
"1. Replace YOUR_CLIENT_API_KEY with your project's client API key (find it with amba_projects_get)",
|
|
1548
1835
|
"2. Configure push notification integration (APNs for iOS, FCM for Android) using amba_integrations_configure",
|
|
1549
1836
|
"3. Create segments to target user groups with amba_segments_create",
|
|
1550
|
-
"4. Set up remote config values with
|
|
1837
|
+
"4. Set up remote config values with amba_remote_configs_create",
|
|
1551
1838
|
"5. Create streaks to track user engagement with amba_streaks_create",
|
|
1552
1839
|
"",
|
|
1553
1840
|
"## Content delivery loop (worked example — DX-12)",
|
|
@@ -1576,7 +1863,7 @@ const criteriaSchema = z.object({
|
|
|
1576
1863
|
property_key: z.string().optional().describe("User property key (required for property_value type)"),
|
|
1577
1864
|
target_value: z.number().describe("Target value to unlock the achievement")
|
|
1578
1865
|
});
|
|
1579
|
-
function registerTools$
|
|
1866
|
+
function registerTools$23(server, apiClient) {
|
|
1580
1867
|
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
1868
|
project_id: z.string().describe("The project ID"),
|
|
1582
1869
|
key: z.string().describe("Unique key for this achievement (e.g. \"first_workout\", \"streak_master_7\")"),
|
|
@@ -1656,10 +1943,32 @@ function registerTools$15(server, apiClient) {
|
|
|
1656
1943
|
text: JSON.stringify(result, null, 2)
|
|
1657
1944
|
}] };
|
|
1658
1945
|
}, ["amba_delete_achievement"]);
|
|
1946
|
+
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.", {
|
|
1947
|
+
project_id: z.string().describe("The project ID"),
|
|
1948
|
+
achievement_id: z.string().describe("The achievement definition ID"),
|
|
1949
|
+
app_user_id: z.string().describe("The user to award the achievement to")
|
|
1950
|
+
}, async ({ project_id, achievement_id, app_user_id }, { client }) => {
|
|
1951
|
+
const result = await client.post(`/projects/${project_id}/achievements/${achievement_id}/grant`, { app_user_id });
|
|
1952
|
+
return { content: [{
|
|
1953
|
+
type: "text",
|
|
1954
|
+
text: JSON.stringify(result, null, 2)
|
|
1955
|
+
}] };
|
|
1956
|
+
});
|
|
1957
|
+
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.", {
|
|
1958
|
+
project_id: z.string().describe("The project ID"),
|
|
1959
|
+
achievement_id: z.string().describe("The achievement definition ID"),
|
|
1960
|
+
app_user_id: z.string().describe("The user to revoke the achievement from")
|
|
1961
|
+
}, async ({ project_id, achievement_id, app_user_id }, { client }) => {
|
|
1962
|
+
const result = await client.post(`/projects/${project_id}/achievements/${achievement_id}/revoke`, { app_user_id });
|
|
1963
|
+
return { content: [{
|
|
1964
|
+
type: "text",
|
|
1965
|
+
text: JSON.stringify(result, null, 2)
|
|
1966
|
+
}] };
|
|
1967
|
+
});
|
|
1659
1968
|
}
|
|
1660
1969
|
//#endregion
|
|
1661
1970
|
//#region src/tools/challenges.ts
|
|
1662
|
-
function registerTools$
|
|
1971
|
+
function registerTools$22(server, apiClient) {
|
|
1663
1972
|
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
1973
|
project_id: z.string().describe("The project ID"),
|
|
1665
1974
|
name: z.string().describe("Challenge name (e.g. \"7-Day Fitness Sprint\", \"XP Weekend Blitz\")"),
|
|
@@ -1770,7 +2079,7 @@ function registerTools$14(server, apiClient) {
|
|
|
1770
2079
|
}
|
|
1771
2080
|
//#endregion
|
|
1772
2081
|
//#region src/tools/economy.ts
|
|
1773
|
-
function registerTools$
|
|
2082
|
+
function registerTools$21(server, apiClient) {
|
|
1774
2083
|
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
2084
|
project_id: z.string().describe("The project ID"),
|
|
1776
2085
|
code: z.string().describe("Unique currency code (e.g. \"gold\", \"gems\", \"hearts\")"),
|
|
@@ -1805,25 +2114,48 @@ function registerTools$13(server, apiClient) {
|
|
|
1805
2114
|
text: JSON.stringify(result, null, 2)
|
|
1806
2115
|
}] };
|
|
1807
2116
|
}, ["amba_list_currencies"]);
|
|
1808
|
-
registerTool(server, apiClient, "amba_currencies_grant", "Grant virtual currency to a specific user. Useful for rewards, promotions, or admin adjustments.", {
|
|
2117
|
+
registerTool(server, apiClient, "amba_currencies_grant", "Grant virtual currency to a specific user. Useful for rewards, promotions, or admin adjustments. Pass idempotency_key to make a grant exactly-once: a repeat with the same key for the same (user, currency) returns the original transaction (HTTP 200, idempotent_replay:true) without re-applying — e.g. encode `share:{userId}:{yyyy-mm-dd}` to cap \"one per share per day\" without a guard collection.", {
|
|
1809
2118
|
project_id: z.string().describe("The project ID"),
|
|
1810
2119
|
app_user_id: z.string().describe("The user to grant currency to"),
|
|
1811
2120
|
currency_code: z.string().describe("Currency code (e.g. \"gold\")"),
|
|
1812
2121
|
amount: z.number().describe("Amount to grant (positive integer)"),
|
|
1813
|
-
reason: z.string().optional().describe("Reason for the grant (for audit trail)")
|
|
1814
|
-
|
|
2122
|
+
reason: z.string().optional().describe("Reason for the grant (for audit trail)"),
|
|
2123
|
+
idempotency_key: z.string().optional().describe("Dedup key (≤255 chars). A repeat with the same key for this (user, currency) returns the original grant without re-applying. The key is permanent — encode the window into it (e.g. include the date for once-per-day).")
|
|
2124
|
+
}, async ({ project_id, app_user_id, currency_code, amount, reason, idempotency_key }, { client }) => {
|
|
1815
2125
|
const payload = {
|
|
1816
2126
|
app_user_id,
|
|
1817
2127
|
currency_code,
|
|
1818
2128
|
amount
|
|
1819
2129
|
};
|
|
1820
2130
|
if (reason !== void 0) payload.reason = reason;
|
|
2131
|
+
if (idempotency_key !== void 0) payload.idempotency_key = idempotency_key;
|
|
1821
2132
|
const result = await client.post(`/projects/${project_id}/currencies/grant`, payload);
|
|
1822
2133
|
return { content: [{
|
|
1823
2134
|
type: "text",
|
|
1824
2135
|
text: JSON.stringify(result, null, 2)
|
|
1825
2136
|
}] };
|
|
1826
2137
|
}, ["amba_grant_currency"]);
|
|
2138
|
+
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. Pass idempotency_key to make a debit exactly-once: a repeat with the same key returns the original debit (HTTP 200, idempotent_replay:true) without double-charging.", {
|
|
2139
|
+
project_id: z.string().describe("The project ID"),
|
|
2140
|
+
app_user_id: z.string().describe("The user to debit currency from"),
|
|
2141
|
+
currency_code: z.string().describe("Currency code (e.g. \"gold\")"),
|
|
2142
|
+
amount: z.number().describe("Positive amount to debit. The endpoint records this as a negative delta on the ledger."),
|
|
2143
|
+
reason: z.string().optional().describe("Reason for the spend (stored as reference_id, useful for the audit trail)"),
|
|
2144
|
+
idempotency_key: z.string().optional().describe("Dedup key (≤255 chars). A repeat with the same key for this (user, currency) returns the original debit without double-charging. A spend that failed with INSUFFICIENT_FUNDS stores no key, so retrying it is allowed.")
|
|
2145
|
+
}, async ({ project_id, app_user_id, currency_code, amount, reason, idempotency_key }, { client }) => {
|
|
2146
|
+
const payload = {
|
|
2147
|
+
app_user_id,
|
|
2148
|
+
currency_code,
|
|
2149
|
+
amount
|
|
2150
|
+
};
|
|
2151
|
+
if (reason !== void 0) payload.reason = reason;
|
|
2152
|
+
if (idempotency_key !== void 0) payload.idempotency_key = idempotency_key;
|
|
2153
|
+
const result = await client.post(`/projects/${project_id}/currencies/spend`, payload);
|
|
2154
|
+
return { content: [{
|
|
2155
|
+
type: "text",
|
|
2156
|
+
text: JSON.stringify(result, null, 2)
|
|
2157
|
+
}] };
|
|
2158
|
+
});
|
|
1827
2159
|
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
2160
|
project_id: z.string().describe("The project ID"),
|
|
1829
2161
|
currency_key: z.string().describe("Currency code to grant (e.g. \"calm_points\")"),
|
|
@@ -1978,6 +2310,23 @@ function registerTools$13(server, apiClient) {
|
|
|
1978
2310
|
text: JSON.stringify(result, null, 2)
|
|
1979
2311
|
}] };
|
|
1980
2312
|
}, ["amba_grant_item"]);
|
|
2313
|
+
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.", {
|
|
2314
|
+
project_id: z.string().describe("The project ID"),
|
|
2315
|
+
app_user_id: z.string().describe("The user to remove the item from"),
|
|
2316
|
+
catalog_item_id: z.string().describe("The catalog item ID to revoke"),
|
|
2317
|
+
quantity: z.number().optional().describe("Quantity to revoke (default 1)")
|
|
2318
|
+
}, async ({ project_id, app_user_id, catalog_item_id, quantity }, { client }) => {
|
|
2319
|
+
const payload = {
|
|
2320
|
+
app_user_id,
|
|
2321
|
+
catalog_item_id
|
|
2322
|
+
};
|
|
2323
|
+
if (quantity !== void 0) payload.quantity = quantity;
|
|
2324
|
+
const result = await client.post(`/projects/${project_id}/inventory/revoke`, payload);
|
|
2325
|
+
return { content: [{
|
|
2326
|
+
type: "text",
|
|
2327
|
+
text: JSON.stringify(result, null, 2)
|
|
2328
|
+
}] };
|
|
2329
|
+
});
|
|
1981
2330
|
registerTool(server, apiClient, "amba_users_get_inventory", "View a user's inventory including all owned items and quantities.", {
|
|
1982
2331
|
project_id: z.string().describe("The project ID"),
|
|
1983
2332
|
app_user_id: z.string().describe("The user ID to look up")
|
|
@@ -2266,8 +2615,8 @@ function registerTools$13(server, apiClient) {
|
|
|
2266
2615
|
}
|
|
2267
2616
|
//#endregion
|
|
2268
2617
|
//#region src/tools/leaderboards.ts
|
|
2269
|
-
function registerTools$
|
|
2270
|
-
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.", {
|
|
2618
|
+
function registerTools$20(server, apiClient) {
|
|
2619
|
+
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. The returned `key` (which equals `name`) is the exact value the in-app SDK passes to `Amba.leaderboards.getEntries(key)` / `getRank(key)`.", {
|
|
2271
2620
|
project_id: z.string().describe("The project ID"),
|
|
2272
2621
|
name: z.string().describe("Leaderboard name (e.g. \"Top XP Earners\", \"Weekly Streak Leaders\")"),
|
|
2273
2622
|
metric: z.enum([
|
|
@@ -2312,7 +2661,7 @@ function registerTools$12(server, apiClient) {
|
|
|
2312
2661
|
text: JSON.stringify(result, null, 2)
|
|
2313
2662
|
}] };
|
|
2314
2663
|
}, ["amba_get_leaderboard"]);
|
|
2315
|
-
registerTool(server, apiClient, "amba_leaderboards_list", "List all leaderboard definitions for a project. Returns name, metric, period, and max_entries per leaderboard. (Use `amba_leaderboards_get` to fetch ranked entries for a specific leaderboard.)", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
2664
|
+
registerTool(server, apiClient, "amba_leaderboards_list", "List all leaderboard definitions for a project. Returns `key` (= name, the handle the in-app SDK `leaderboards.getEntries(key)` expects), name, metric, period, and max_entries per leaderboard. (Use `amba_leaderboards_get` to fetch ranked entries for a specific leaderboard.)", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
2316
2665
|
const result = await client.get(`/projects/${project_id}/leaderboards`);
|
|
2317
2666
|
return { content: [{
|
|
2318
2667
|
type: "text",
|
|
@@ -2371,8 +2720,80 @@ function registerTools$12(server, apiClient) {
|
|
|
2371
2720
|
}, ["amba_delete_leaderboard"]);
|
|
2372
2721
|
}
|
|
2373
2722
|
//#endregion
|
|
2723
|
+
//#region src/tools/leagues.ts
|
|
2724
|
+
/**
|
|
2725
|
+
* Leagues — the Duolingo-style promote/demote tiers (Bronze/Silver/Gold/…).
|
|
2726
|
+
* Distinct from leaderboards (a single ranked list): a league has weekly
|
|
2727
|
+
* cohorts that the rollover workflow reshuffles. These tools provision and
|
|
2728
|
+
* inspect leagues; the admin routes are mounted at
|
|
2729
|
+
* `/v1/admin/projects/:projectId/leagues`. Members are assigned by the weekly
|
|
2730
|
+
* rollover (the registered Temporal schedule) and score live off `xp_awarded`
|
|
2731
|
+
* engagement events.
|
|
2732
|
+
*/
|
|
2733
|
+
function registerTools$19(server, apiClient) {
|
|
2734
|
+
registerTool(server, apiClient, "amba_leagues_create", "Create a league tier (e.g. \"Bronze\", tier_order=1). Leagues are the promote/demote ladder: each week users are grouped into cohorts of `cohort_size`, the top `promote_count` move up a tier and the bottom `demote_count` move down. Create one league per tier (lowest tier_order = entry tier). Members are assigned by the weekly rollover, not at create time.", {
|
|
2735
|
+
project_id: z.string().describe("The project ID"),
|
|
2736
|
+
name: z.string().describe("League tier name, e.g. \"Bronze\", \"Silver\", \"Gold\""),
|
|
2737
|
+
tier_order: z.number().describe("Tier rank, 1 = lowest/entry tier. Higher tiers have higher tier_order."),
|
|
2738
|
+
promote_count: z.number().optional().describe("How many top members of a cohort promote each week (default 5)"),
|
|
2739
|
+
demote_count: z.number().optional().describe("How many bottom members of a cohort demote each week (default 5)"),
|
|
2740
|
+
cohort_size: z.number().optional().describe("Target members per weekly cohort (default 30)")
|
|
2741
|
+
}, async ({ project_id, name, tier_order, promote_count, demote_count, cohort_size }, { client }) => {
|
|
2742
|
+
const payload = {
|
|
2743
|
+
name,
|
|
2744
|
+
tier_order
|
|
2745
|
+
};
|
|
2746
|
+
if (promote_count !== void 0) payload.promote_count = promote_count;
|
|
2747
|
+
if (demote_count !== void 0) payload.demote_count = demote_count;
|
|
2748
|
+
if (cohort_size !== void 0) payload.cohort_size = cohort_size;
|
|
2749
|
+
const result = await client.post(`/projects/${project_id}/leagues`, payload);
|
|
2750
|
+
return { content: [{
|
|
2751
|
+
type: "text",
|
|
2752
|
+
text: JSON.stringify(result, null, 2)
|
|
2753
|
+
}] };
|
|
2754
|
+
}, ["amba_create_league"]);
|
|
2755
|
+
registerTool(server, apiClient, "amba_leagues_list", "List all league tiers for a project, ordered by tier. Returns name, tier_order, promote_count, demote_count, and cohort_size per league.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
2756
|
+
const result = await client.get(`/projects/${project_id}/leagues`);
|
|
2757
|
+
return { content: [{
|
|
2758
|
+
type: "text",
|
|
2759
|
+
text: JSON.stringify(result, null, 2)
|
|
2760
|
+
}] };
|
|
2761
|
+
}, ["amba_list_leagues"]);
|
|
2762
|
+
registerTool(server, apiClient, "amba_leagues_update", "Partially update a league tier. Only the supplied fields change. `tier_order` is intentionally immutable (re-ranking mid-week would require re-cohorting every active membership).", {
|
|
2763
|
+
project_id: z.string().describe("The project ID"),
|
|
2764
|
+
league_id: z.string().describe("The league ID"),
|
|
2765
|
+
name: z.string().optional().describe("New tier name"),
|
|
2766
|
+
promote_count: z.number().optional().describe("New weekly promote count"),
|
|
2767
|
+
demote_count: z.number().optional().describe("New weekly demote count"),
|
|
2768
|
+
cohort_size: z.number().optional().describe("New target cohort size"),
|
|
2769
|
+
is_active: z.boolean().optional().describe("Enable/disable the tier")
|
|
2770
|
+
}, async ({ project_id, league_id, name, promote_count, demote_count, cohort_size, is_active }, { client }) => {
|
|
2771
|
+
const payload = {};
|
|
2772
|
+
if (name !== void 0) payload.name = name;
|
|
2773
|
+
if (promote_count !== void 0) payload.promote_count = promote_count;
|
|
2774
|
+
if (demote_count !== void 0) payload.demote_count = demote_count;
|
|
2775
|
+
if (cohort_size !== void 0) payload.cohort_size = cohort_size;
|
|
2776
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
2777
|
+
const result = await client.patch(`/projects/${project_id}/leagues/${league_id}`, payload);
|
|
2778
|
+
return { content: [{
|
|
2779
|
+
type: "text",
|
|
2780
|
+
text: JSON.stringify(result, null, 2)
|
|
2781
|
+
}] };
|
|
2782
|
+
}, ["amba_update_league"]);
|
|
2783
|
+
registerTool(server, apiClient, "amba_leagues_list_cohorts", "List this week's active cohorts for a league, each with a member_count. Cohorts (and their memberships) are created by the weekly rollover — a freshly created league shows no cohorts until the first rollover runs.", {
|
|
2784
|
+
project_id: z.string().describe("The project ID"),
|
|
2785
|
+
league_id: z.string().describe("The league ID")
|
|
2786
|
+
}, async ({ project_id, league_id }, { client }) => {
|
|
2787
|
+
const result = await client.get(`/projects/${project_id}/leagues/${league_id}/cohorts/current`);
|
|
2788
|
+
return { content: [{
|
|
2789
|
+
type: "text",
|
|
2790
|
+
text: JSON.stringify(result, null, 2)
|
|
2791
|
+
}] };
|
|
2792
|
+
}, ["amba_list_league_cohorts"]);
|
|
2793
|
+
}
|
|
2794
|
+
//#endregion
|
|
2374
2795
|
//#region src/tools/platform.ts
|
|
2375
|
-
function registerTools$
|
|
2796
|
+
function registerTools$18(server, apiClient) {
|
|
2376
2797
|
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
2798
|
project_id: z.string().describe("The project ID"),
|
|
2378
2799
|
name: z.string().describe("Flow name (e.g. \"Welcome Flow\", \"Premium Onboarding\")"),
|
|
@@ -2791,6 +3212,44 @@ function registerTools$11(server, apiClient) {
|
|
|
2791
3212
|
text: JSON.stringify(result, null, 2)
|
|
2792
3213
|
}] };
|
|
2793
3214
|
});
|
|
3215
|
+
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.", {
|
|
3216
|
+
project_id: z.string().describe("The project ID"),
|
|
3217
|
+
type: z.enum(["direct", "group"]).optional().describe("Conversation type. Defaults to \"group\"."),
|
|
3218
|
+
name: z.string().optional().describe("Optional conversation name (group conversations)."),
|
|
3219
|
+
participant_ids: z.array(z.string()).optional().describe("Initial participant app_user IDs. May be empty or omitted.")
|
|
3220
|
+
}, async ({ project_id, type, name, participant_ids }, { client }) => {
|
|
3221
|
+
const payload = {};
|
|
3222
|
+
if (type !== void 0) payload.type = type;
|
|
3223
|
+
if (name !== void 0) payload.name = name;
|
|
3224
|
+
if (participant_ids !== void 0) payload.participant_ids = participant_ids;
|
|
3225
|
+
const result = await client.post(`/projects/${project_id}/messaging/conversations`, payload);
|
|
3226
|
+
return { content: [{
|
|
3227
|
+
type: "text",
|
|
3228
|
+
text: JSON.stringify(result, null, 2)
|
|
3229
|
+
}] };
|
|
3230
|
+
});
|
|
3231
|
+
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.", {
|
|
3232
|
+
project_id: z.string().describe("The project ID"),
|
|
3233
|
+
conversation_id: z.string().describe("The conversation ID"),
|
|
3234
|
+
user_id: z.string().describe("The app_user ID to add")
|
|
3235
|
+
}, async ({ project_id, conversation_id, user_id }, { client }) => {
|
|
3236
|
+
const result = await client.post(`/projects/${project_id}/messaging/conversations/${conversation_id}/participants`, { user_id });
|
|
3237
|
+
return { content: [{
|
|
3238
|
+
type: "text",
|
|
3239
|
+
text: JSON.stringify(result, null, 2)
|
|
3240
|
+
}] };
|
|
3241
|
+
});
|
|
3242
|
+
registerTool(server, apiClient, "amba_messaging_remove_participant", "Remove a user from a conversation (idempotent inverse of add). Removing a non-member returns removed: false.", {
|
|
3243
|
+
project_id: z.string().describe("The project ID"),
|
|
3244
|
+
conversation_id: z.string().describe("The conversation ID"),
|
|
3245
|
+
user_id: z.string().describe("The app_user ID to remove")
|
|
3246
|
+
}, async ({ project_id, conversation_id, user_id }, { client }) => {
|
|
3247
|
+
const result = await client.delete(`/projects/${project_id}/messaging/conversations/${conversation_id}/participants/${user_id}`);
|
|
3248
|
+
return { content: [{
|
|
3249
|
+
type: "text",
|
|
3250
|
+
text: JSON.stringify(result, null, 2)
|
|
3251
|
+
}] };
|
|
3252
|
+
});
|
|
2794
3253
|
registerTool(server, apiClient, "amba_messaging_list_conversations", "List message conversations for a project, newest first. Pagination via limit+offset.", {
|
|
2795
3254
|
project_id: z.string().describe("The project ID"),
|
|
2796
3255
|
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
@@ -2910,7 +3369,7 @@ function registerTools$11(server, apiClient) {
|
|
|
2910
3369
|
}
|
|
2911
3370
|
//#endregion
|
|
2912
3371
|
//#region src/tools/social.ts
|
|
2913
|
-
function registerTools$
|
|
3372
|
+
function registerTools$17(server, apiClient) {
|
|
2914
3373
|
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
3374
|
const result = await client.get(`/projects/${project_id}/friends/stats`);
|
|
2916
3375
|
return { content: [{
|
|
@@ -3235,7 +3694,7 @@ function registerTools$10(server, apiClient) {
|
|
|
3235
3694
|
}
|
|
3236
3695
|
//#endregion
|
|
3237
3696
|
//#region src/tools/xp.ts
|
|
3238
|
-
function registerTools$
|
|
3697
|
+
function registerTools$16(server, apiClient) {
|
|
3239
3698
|
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
3699
|
project_id: z.string().describe("The project ID"),
|
|
3241
3700
|
name: z.string().describe("Rule name (e.g. \"Workout Completed\", \"Daily Login Bonus\")"),
|
|
@@ -3307,6 +3766,40 @@ function registerTools$9(server, apiClient) {
|
|
|
3307
3766
|
text: JSON.stringify(result, null, 2)
|
|
3308
3767
|
}] };
|
|
3309
3768
|
});
|
|
3769
|
+
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.", {
|
|
3770
|
+
project_id: z.string().describe("The project ID"),
|
|
3771
|
+
app_user_id: z.string().describe("The user to grant XP to"),
|
|
3772
|
+
amount: z.number().describe("Positive amount of XP to add"),
|
|
3773
|
+
reason: z.string().optional().describe("Reason for the grant (stored on the ledger row for the audit trail)")
|
|
3774
|
+
}, async ({ project_id, app_user_id, amount, reason }, { client }) => {
|
|
3775
|
+
const payload = {
|
|
3776
|
+
app_user_id,
|
|
3777
|
+
amount
|
|
3778
|
+
};
|
|
3779
|
+
if (reason !== void 0) payload.reason = reason;
|
|
3780
|
+
const result = await client.post(`/projects/${project_id}/xp/grant`, payload);
|
|
3781
|
+
return { content: [{
|
|
3782
|
+
type: "text",
|
|
3783
|
+
text: JSON.stringify(result, null, 2)
|
|
3784
|
+
}] };
|
|
3785
|
+
});
|
|
3786
|
+
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.", {
|
|
3787
|
+
project_id: z.string().describe("The project ID"),
|
|
3788
|
+
app_user_id: z.string().describe("The user to deduct XP from"),
|
|
3789
|
+
amount: z.number().describe("Positive amount of XP to subtract"),
|
|
3790
|
+
reason: z.string().optional().describe("Reason for the deduction (stored on the ledger row for the audit trail)")
|
|
3791
|
+
}, async ({ project_id, app_user_id, amount, reason }, { client }) => {
|
|
3792
|
+
const payload = {
|
|
3793
|
+
app_user_id,
|
|
3794
|
+
amount
|
|
3795
|
+
};
|
|
3796
|
+
if (reason !== void 0) payload.reason = reason;
|
|
3797
|
+
const result = await client.post(`/projects/${project_id}/xp/deduct`, payload);
|
|
3798
|
+
return { content: [{
|
|
3799
|
+
type: "text",
|
|
3800
|
+
text: JSON.stringify(result, null, 2)
|
|
3801
|
+
}] };
|
|
3802
|
+
});
|
|
3310
3803
|
registerTool(server, apiClient, "amba_xp_list_users", "List users by total XP (leaderboard view). Ordered by total_xp descending with pagination.", {
|
|
3311
3804
|
project_id: z.string().describe("The project ID"),
|
|
3312
3805
|
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
@@ -3324,7 +3817,7 @@ function registerTools$9(server, apiClient) {
|
|
|
3324
3817
|
}
|
|
3325
3818
|
//#endregion
|
|
3326
3819
|
//#region src/tools/events.ts
|
|
3327
|
-
function registerTools$
|
|
3820
|
+
function registerTools$15(server, apiClient) {
|
|
3328
3821
|
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
3822
|
project_id: z.string().describe("The project ID"),
|
|
3330
3823
|
since: z.string().optional().describe("ISO-8601 lower bound for occurred_at. Defaults to 24h ago."),
|
|
@@ -3366,57 +3859,176 @@ function registerTools$8(server, apiClient) {
|
|
|
3366
3859
|
});
|
|
3367
3860
|
}
|
|
3368
3861
|
//#endregion
|
|
3369
|
-
//#region src/
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
}
|
|
3418
|
-
|
|
3419
|
-
|
|
3862
|
+
//#region src/tools/funnels.ts
|
|
3863
|
+
const funnelFilterSchema = z.object({
|
|
3864
|
+
property: z.string().describe("Dot-path into the event `properties` JSON (e.g. \"source\" or \"plan.tier\")."),
|
|
3865
|
+
op: z.enum([
|
|
3866
|
+
"eq",
|
|
3867
|
+
"neq",
|
|
3868
|
+
"gt",
|
|
3869
|
+
"gte",
|
|
3870
|
+
"lt",
|
|
3871
|
+
"lte",
|
|
3872
|
+
"contains",
|
|
3873
|
+
"not_contains",
|
|
3874
|
+
"exists",
|
|
3875
|
+
"not_exists"
|
|
3876
|
+
]).describe("Comparison operator. exists/not_exists ignore `value`."),
|
|
3877
|
+
value: z.union([
|
|
3878
|
+
z.string(),
|
|
3879
|
+
z.number(),
|
|
3880
|
+
z.boolean()
|
|
3881
|
+
]).optional().describe("Scalar to compare against. Required for every op except exists/not_exists.")
|
|
3882
|
+
});
|
|
3883
|
+
const funnelStepSchema = z.object({
|
|
3884
|
+
event: z.string().describe("Event name to match (engagement event_name)."),
|
|
3885
|
+
filters: z.array(funnelFilterSchema).optional().describe("Optional property predicates, ANDed together, that an event must satisfy.")
|
|
3886
|
+
});
|
|
3887
|
+
function registerTools$14(server, apiClient) {
|
|
3888
|
+
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 }) => {
|
|
3889
|
+
const result = await client.get(`/projects/${project_id}/funnels`);
|
|
3890
|
+
return { content: [{
|
|
3891
|
+
type: "text",
|
|
3892
|
+
text: JSON.stringify(result, null, 2)
|
|
3893
|
+
}] };
|
|
3894
|
+
});
|
|
3895
|
+
registerTool(server, apiClient, "amba_funnels_create", [
|
|
3896
|
+
"Create a conversion funnel definition. Steps are ORDERED: a user is credited with",
|
|
3897
|
+
"step N only if they emitted step N after their matched step N-1, all within the",
|
|
3898
|
+
"conversion window measured from their first step-0 event. conversion_window_seconds",
|
|
3899
|
+
"defaults to 604800 (7 days). Needs at least 2 steps."
|
|
3900
|
+
].join(" "), {
|
|
3901
|
+
project_id: z.string().describe("The project ID"),
|
|
3902
|
+
name: z.string().describe("Funnel name (unique per project)."),
|
|
3903
|
+
description: z.string().optional().describe("Human-readable description."),
|
|
3904
|
+
steps: z.array(funnelStepSchema).describe("Ordered steps (2-10). Each step is an event + optional property filters."),
|
|
3905
|
+
conversion_window_seconds: z.number().int().optional().describe("Conversion window in seconds (60 .. 7776000). Defaults to 604800 (7 days).")
|
|
3906
|
+
}, async ({ project_id, name, description, steps, conversion_window_seconds }, { client }) => {
|
|
3907
|
+
const payload = {
|
|
3908
|
+
name,
|
|
3909
|
+
steps
|
|
3910
|
+
};
|
|
3911
|
+
if (description !== void 0) payload.description = description;
|
|
3912
|
+
if (conversion_window_seconds !== void 0) payload.conversion_window_seconds = conversion_window_seconds;
|
|
3913
|
+
const result = await client.post(`/projects/${project_id}/funnels`, payload);
|
|
3914
|
+
return { content: [{
|
|
3915
|
+
type: "text",
|
|
3916
|
+
text: JSON.stringify(result, null, 2)
|
|
3917
|
+
}] };
|
|
3918
|
+
});
|
|
3919
|
+
registerTool(server, apiClient, "amba_funnels_get", "Get a single funnel definition by id (name, ordered steps, conversion window).", {
|
|
3920
|
+
project_id: z.string().describe("The project ID"),
|
|
3921
|
+
funnel_id: z.string().describe("The funnel ID")
|
|
3922
|
+
}, async ({ project_id, funnel_id }, { client }) => {
|
|
3923
|
+
const result = await client.get(`/projects/${project_id}/funnels/${funnel_id}`);
|
|
3924
|
+
return { content: [{
|
|
3925
|
+
type: "text",
|
|
3926
|
+
text: JSON.stringify(result, null, 2)
|
|
3927
|
+
}] };
|
|
3928
|
+
});
|
|
3929
|
+
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.", {
|
|
3930
|
+
project_id: z.string().describe("The project ID"),
|
|
3931
|
+
funnel_id: z.string().describe("The funnel ID"),
|
|
3932
|
+
name: z.string().optional().describe("New funnel name."),
|
|
3933
|
+
description: z.string().nullable().optional().describe("New description; pass null to clear."),
|
|
3934
|
+
steps: z.array(funnelStepSchema).optional().describe("Replacement ordered step list (2-10)."),
|
|
3935
|
+
conversion_window_seconds: z.number().int().optional().describe("New conversion window in seconds (60 .. 7776000).")
|
|
3936
|
+
}, async ({ project_id, funnel_id, name, description, steps, conversion_window_seconds }, { client }) => {
|
|
3937
|
+
const payload = {};
|
|
3938
|
+
if (name !== void 0) payload.name = name;
|
|
3939
|
+
if (description !== void 0) payload.description = description;
|
|
3940
|
+
if (steps !== void 0) payload.steps = steps;
|
|
3941
|
+
if (conversion_window_seconds !== void 0) payload.conversion_window_seconds = conversion_window_seconds;
|
|
3942
|
+
const result = await client.patch(`/projects/${project_id}/funnels/${funnel_id}`, payload);
|
|
3943
|
+
return { content: [{
|
|
3944
|
+
type: "text",
|
|
3945
|
+
text: JSON.stringify(result, null, 2)
|
|
3946
|
+
}] };
|
|
3947
|
+
});
|
|
3948
|
+
registerTool(server, apiClient, "amba_funnels_delete", "Delete a funnel definition by id.", {
|
|
3949
|
+
project_id: z.string().describe("The project ID"),
|
|
3950
|
+
funnel_id: z.string().describe("The funnel ID")
|
|
3951
|
+
}, async ({ project_id, funnel_id }, { client }) => {
|
|
3952
|
+
const result = await client.delete(`/projects/${project_id}/funnels/${funnel_id}`);
|
|
3953
|
+
return { content: [{
|
|
3954
|
+
type: "text",
|
|
3955
|
+
text: JSON.stringify(result, null, 2)
|
|
3956
|
+
}] };
|
|
3957
|
+
});
|
|
3958
|
+
registerTool(server, apiClient, "amba_funnels_query", [
|
|
3959
|
+
"Compute a funnel: per-step distinct-user counts, conversion rates, and drop-off over",
|
|
3960
|
+
"an entry date range. `from`/`to` (ISO-8601) bound which step-0 events count as funnel",
|
|
3961
|
+
"entries (default: last 30 days). Each entrant then has their own conversion window in",
|
|
3962
|
+
"which later steps must occur (it may extend past `to`). Returns total_entered,",
|
|
3963
|
+
"total_converted, overall_conversion_rate, and a per-step breakdown."
|
|
3964
|
+
].join(" "), {
|
|
3965
|
+
project_id: z.string().describe("The project ID"),
|
|
3966
|
+
funnel_id: z.string().describe("The funnel ID"),
|
|
3967
|
+
from: z.string().optional().describe("ISO-8601 lower bound for funnel entries. Defaults to 30 days ago."),
|
|
3968
|
+
to: z.string().optional().describe("ISO-8601 upper bound for funnel entries. Defaults to now.")
|
|
3969
|
+
}, async ({ project_id, funnel_id, from, to }, { client }) => {
|
|
3970
|
+
const query = {};
|
|
3971
|
+
if (from !== void 0) query.from = from;
|
|
3972
|
+
if (to !== void 0) query.to = to;
|
|
3973
|
+
const result = await client.get(`/projects/${project_id}/funnels/${funnel_id}/query`, query);
|
|
3974
|
+
return { content: [{
|
|
3975
|
+
type: "text",
|
|
3976
|
+
text: JSON.stringify(result, null, 2)
|
|
3977
|
+
}] };
|
|
3978
|
+
});
|
|
3979
|
+
}
|
|
3980
|
+
//#endregion
|
|
3981
|
+
//#region src/lib/tool-result.ts
|
|
3982
|
+
/**
|
|
3983
|
+
* Shared MCP tool result helpers.
|
|
3984
|
+
*
|
|
3985
|
+
* Every tool emits the same `{ content: [{ type: 'text', text: <json> }] }`
|
|
3986
|
+
* envelope. Two helpers centralize that:
|
|
3987
|
+
*
|
|
3988
|
+
* - [`jsonResult`] — wraps an arbitrary payload.
|
|
3989
|
+
* - [`passthroughResult`] — flattens an upstream HTTP response
|
|
3990
|
+
* (status + parsed body) into the same envelope. Status is written
|
|
3991
|
+
* LAST so a colliding top-level `status` field in the API response
|
|
3992
|
+
* cannot shadow the HTTP status — agents look at `parsed.status` to
|
|
3993
|
+
* distinguish 2xx from 4xx/5xx.
|
|
3994
|
+
*
|
|
3995
|
+
* Lives in `src/lib/` (vs. `src/tools/_helpers.ts`) to set the same
|
|
3996
|
+
* cross-cutting-helper precedent as `src/lib/with-pat.ts` (task #36).
|
|
3997
|
+
* `tools/*` files stay strictly tool registrations.
|
|
3998
|
+
*/
|
|
3999
|
+
/**
|
|
4000
|
+
* Wire-shape every MCP tool handler returns.
|
|
4001
|
+
*
|
|
4002
|
+
* The MCP SDK's `tool()` callback signature is structurally typed and
|
|
4003
|
+
* carries an open index signature for `_meta` etc. Declaring our return
|
|
4004
|
+
* type as a plain `{ content: [...] }` interface won't satisfy that
|
|
4005
|
+
* structural check — so the helpers' return type is left as the actual
|
|
4006
|
+
* inferred shape (no explicit interface) and consumers rely on the
|
|
4007
|
+
* inference + the SDK's structural compatibility. If we ever want a
|
|
4008
|
+
* named alias, write it as a type-alias over the inferred shape rather
|
|
4009
|
+
* than a closed interface.
|
|
4010
|
+
*/
|
|
4011
|
+
/** Wrap an arbitrary payload as a JSON-text tool result. */
|
|
4012
|
+
function jsonResult$1(payload) {
|
|
4013
|
+
return { content: [{
|
|
4014
|
+
type: "text",
|
|
4015
|
+
text: JSON.stringify(payload, null, 2)
|
|
4016
|
+
}] };
|
|
4017
|
+
}
|
|
4018
|
+
/**
|
|
4019
|
+
* Flatten an upstream HTTP response into the agent-facing tool payload.
|
|
4020
|
+
* Body fields are spread FIRST so a future top-level `status` key in
|
|
4021
|
+
* the API response cannot shadow the HTTP `status` — agents read
|
|
4022
|
+
* `parsed.status` to distinguish 2xx from 4xx/5xx.
|
|
4023
|
+
*/
|
|
4024
|
+
function passthroughResult(result) {
|
|
4025
|
+
return jsonResult$1({
|
|
4026
|
+
...result.body ?? {},
|
|
4027
|
+
status: result.status
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
//#endregion
|
|
4031
|
+
//#region src/tools/auth.ts
|
|
3420
4032
|
async function authFetch(apiClient, options) {
|
|
3421
4033
|
const url = `${apiClient.getApiRoot()}${options.path}`;
|
|
3422
4034
|
const headers = { Accept: "application/json" };
|
|
@@ -3538,7 +4150,7 @@ function enrichedAuthResult(result, agentInstructions) {
|
|
|
3538
4150
|
status: result.status
|
|
3539
4151
|
});
|
|
3540
4152
|
}
|
|
3541
|
-
function registerTools$
|
|
4153
|
+
function registerTools$13(server, apiClient) {
|
|
3542
4154
|
registerPublicTool(server, "amba_developer_signup", [
|
|
3543
4155
|
"Create a new Amba developer account. Returns a long-lived Personal Access Token (PAT)",
|
|
3544
4156
|
"plus a real isolated Amba project (provisioning asynchronously), plus ready-to-paste",
|
|
@@ -3672,6 +4284,18 @@ async function adminFetch(apiClient, options) {
|
|
|
3672
4284
|
body
|
|
3673
4285
|
};
|
|
3674
4286
|
}
|
|
4287
|
+
const CATALOG_SHAPE_KEYS = [
|
|
4288
|
+
"is_published",
|
|
4289
|
+
"published_at",
|
|
4290
|
+
"is_public",
|
|
4291
|
+
"published"
|
|
4292
|
+
];
|
|
4293
|
+
function catalogShapeWarning(row) {
|
|
4294
|
+
const hit = CATALOG_SHAPE_KEYS.filter((k) => k in row);
|
|
4295
|
+
if (hit.length === 0) return null;
|
|
4296
|
+
const ownership = "user_id" in row ? `attributed to user_id "${String(row["user_id"])}"` : "attributed to the seeding user (or rejected if the collection requires user_id)";
|
|
4297
|
+
return [`⚠️ This row has catalog-shaped column(s) [${hit.join(", ")}] but is being inserted into a per-user collection, so it will be ${ownership} — other users will NOT see it via the client SDK.`, "If this is shared/global content (a catalog, articles, a media library), use a Content Library (amba_content_libraries_create) or create the collection with shared:true so its rows are readable by every user. Proceeding anyway."].join(" ");
|
|
4298
|
+
}
|
|
3675
4299
|
async function clientFetch(apiClient, options) {
|
|
3676
4300
|
let url = `${apiClient.getApiRoot()}/client${options.path}`;
|
|
3677
4301
|
if (options.query && Object.keys(options.query).length > 0) url += `?${new URLSearchParams(options.query).toString()}`;
|
|
@@ -3709,7 +4333,7 @@ const MISSING_API_KEY_ERROR = jsonResult$1({
|
|
|
3709
4333
|
});
|
|
3710
4334
|
const columnSchema = z.object({
|
|
3711
4335
|
name: z.string().describe("Column name. Must match /^[a-z][a-z0-9_]*$/."),
|
|
3712
|
-
type: z.string().describe("Column type. One of: text, integer, bigint, boolean, jsonb, timestamptz, uuid, numeric,
|
|
4336
|
+
type: z.string().describe("Column type. One of: text, integer, bigint, boolean, jsonb, timestamptz, date, uuid, numeric, vector, or an array type (text[], integer[], bigint[], numeric[], boolean[], uuid[]). Array columns support the contains/containedBy/overlaps query operators. Vector columns require a \"dimensions\" hint."),
|
|
3713
4337
|
nullable: z.boolean().optional().describe("Whether the column accepts NULL. Defaults to true."),
|
|
3714
4338
|
default: z.unknown().optional().describe("Default SQL literal (e.g. \"NOW()\", 0, \"''\"). Optional."),
|
|
3715
4339
|
dimensions: z.number().optional().describe("Required for `vector` columns; ignored otherwise.")
|
|
@@ -3735,7 +4359,7 @@ const sdkOrderSchema = z.array(z.object({
|
|
|
3735
4359
|
})).describe("Ordering. `[{column: \"created_at\", direction: \"desc\"}, ...]`.");
|
|
3736
4360
|
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
4361
|
const setSchema = z.record(z.unknown()).describe("Column-value map. Server-managed columns (id, created_at, etc.) are rejected.");
|
|
3738
|
-
function registerTools$
|
|
4362
|
+
function registerTools$12(server, apiClient) {
|
|
3739
4363
|
registerTool(server, apiClient, "amba_collections_create", [
|
|
3740
4364
|
"Create a new collection (schema-first Postgres table) in a project.",
|
|
3741
4365
|
"The DDL is emitted server-side and applied via a Temporal saga.",
|
|
@@ -3746,13 +4370,15 @@ function registerTools$6(server, apiClient) {
|
|
|
3746
4370
|
project_id: z.string().describe("The project ID owning the collection."),
|
|
3747
4371
|
name: z.string().describe("Collection name. Must match /^[a-z][a-z0-9_]*$/. Reserved prefixes (amba_, coll_amba_) are rejected."),
|
|
3748
4372
|
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
|
-
|
|
4373
|
+
indexes: z.array(indexSchema).optional().describe("Optional extra indexes (server creates a primary key + user_id index by default)."),
|
|
4374
|
+
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).")
|
|
4375
|
+
}, async ({ project_id, name, columns, indexes, shared }, { pat }) => {
|
|
3751
4376
|
const body = {
|
|
3752
4377
|
name,
|
|
3753
4378
|
columns
|
|
3754
4379
|
};
|
|
3755
4380
|
if (indexes !== void 0) body.indexes = indexes;
|
|
4381
|
+
if (shared !== void 0) body.shared = shared;
|
|
3756
4382
|
return passthroughResult(await adminFetch(apiClient, {
|
|
3757
4383
|
method: "POST",
|
|
3758
4384
|
path: `/projects/${project_id}/collections`,
|
|
@@ -3810,22 +4436,39 @@ function registerTools$6(server, apiClient) {
|
|
|
3810
4436
|
}));
|
|
3811
4437
|
}, ["amba_delete_collection"]);
|
|
3812
4438
|
registerTool(server, apiClient, "amba_collections_alter", [
|
|
3813
|
-
"Alter a collection: exactly one of `add_column`, `add_index`,
|
|
4439
|
+
"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
4440
|
"Drop-column is destructive — requires `confirm` to equal the dropped column name.",
|
|
4441
|
+
"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.",
|
|
4442
|
+
"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).",
|
|
4443
|
+
"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
4444
|
"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."
|
|
4445
|
+
"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
4446
|
].join(" "), {
|
|
3818
4447
|
project_id: z.string().describe("The project ID."),
|
|
3819
4448
|
name: z.string().describe("Collection name to alter."),
|
|
3820
|
-
add_column: columnSchema.optional().describe("Column to add. Mutually exclusive with
|
|
3821
|
-
add_index: indexSchema.optional().describe("Index to add. Mutually exclusive with
|
|
4449
|
+
add_column: columnSchema.optional().describe("Column to add. Mutually exclusive with the other alter operations."),
|
|
4450
|
+
add_index: indexSchema.optional().describe("Index to add. Mutually exclusive with the other alter operations."),
|
|
4451
|
+
change_type: z.object({
|
|
4452
|
+
column: z.string().describe("Existing column to change."),
|
|
4453
|
+
to_type: z.string().describe("Target type (text, integer, bigint, numeric, boolean, timestamptz, date, jsonb, uuid)."),
|
|
4454
|
+
from_type: z.string().optional().describe("Optional current type. When provided, known-safe widening casts (integer→bigint, integer/bigint→numeric) skip the `using` requirement."),
|
|
4455
|
+
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.")
|
|
4456
|
+
}).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."),
|
|
4457
|
+
rename_column: z.object({
|
|
4458
|
+
from: z.string().describe("Current column name."),
|
|
4459
|
+
to: z.string().describe("New column name. Must match /^[a-z][a-z0-9_]*$/.")
|
|
4460
|
+
}).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
4461
|
drop_column: z.string().optional().describe("Column name to drop. Requires `confirm` query param to match."),
|
|
4462
|
+
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
4463
|
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 }) => {
|
|
4464
|
+
}, async ({ project_id, name, add_column, add_index, change_type, rename_column, drop_column, relax_user_id, confirm }, { pat }) => {
|
|
3825
4465
|
const body = {};
|
|
3826
4466
|
if (add_column !== void 0) body.add_column = add_column;
|
|
3827
4467
|
if (add_index !== void 0) body.add_index = add_index;
|
|
4468
|
+
if (change_type !== void 0) body.change_type = change_type;
|
|
4469
|
+
if (rename_column !== void 0) body.rename_column = rename_column;
|
|
3828
4470
|
if (drop_column !== void 0) body.drop_column = drop_column;
|
|
4471
|
+
if (relax_user_id !== void 0) body.relax_user_id = relax_user_id;
|
|
3829
4472
|
const query = {};
|
|
3830
4473
|
if (drop_column !== void 0 && confirm !== void 0) query.confirm = confirm;
|
|
3831
4474
|
return passthroughResult(await adminFetch(apiClient, {
|
|
@@ -3839,23 +4482,67 @@ function registerTools$6(server, apiClient) {
|
|
|
3839
4482
|
registerTool(server, apiClient, "amba_admin_insert_row", [
|
|
3840
4483
|
"Insert a row directly into a collection from the developer/agent side — BYPASSES auto-RLS.",
|
|
3841
4484
|
"Server-managed columns (id, created_at, updated_at, deleted_at) are stripped from the body. `user_id` is honored if present (admins can create rows attributed to any app_user).",
|
|
4485
|
+
"Atomic upsert: pass on_conflict=\"ignore\" (return the existing row, no change) or \"update\" (merge) plus conflict_target (the unique-index columns). Replaces read-then-write races.",
|
|
3842
4486
|
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
3843
|
-
"Errors: 400 INVALID_COLUMN, 404 COLLECTION_NOT_FOUND, 500 CREATE_FAILED."
|
|
4487
|
+
"Errors: 400 INVALID_COLUMN / INVALID_CONFLICT_TARGET, 404 COLLECTION_NOT_FOUND, 500 CREATE_FAILED."
|
|
3844
4488
|
].join(" "), {
|
|
3845
4489
|
project_id: z.string().describe("The project ID."),
|
|
3846
4490
|
name: z.string().describe("Collection name."),
|
|
3847
|
-
row: z.record(z.unknown()).describe("Column-value map for the new row. Server-managed columns are stripped.")
|
|
3848
|
-
|
|
3849
|
-
|
|
4491
|
+
row: z.record(z.unknown()).describe("Column-value map for the new row. Server-managed columns are stripped."),
|
|
4492
|
+
on_conflict: z.enum([
|
|
4493
|
+
"error",
|
|
4494
|
+
"ignore",
|
|
4495
|
+
"update"
|
|
4496
|
+
]).optional().describe("Conflict policy. \"error\" (default) fails on a unique-constraint clash; \"ignore\" returns the existing row (HTTP 200) unchanged; \"update\" merges the provided columns. Requires conflict_target."),
|
|
4497
|
+
conflict_target: z.array(z.string()).optional().describe("Columns that form the unique index to conflict on (e.g. [\"user_id\",\"kind\"]). Required when on_conflict is \"ignore\" or \"update\"."),
|
|
4498
|
+
return_minimal: z.boolean().optional().describe("When true, the response returns only the inserted row id instead of the full row. Use this for rows with large columns (big JSON blobs) to avoid bloating the response — essential for bulk migrations where echoing full rows exhausts the token budget.")
|
|
4499
|
+
}, async ({ project_id, name, row, on_conflict, conflict_target, return_minimal }, { pat }) => {
|
|
4500
|
+
const query = {};
|
|
4501
|
+
if (on_conflict !== void 0) query.on_conflict = on_conflict;
|
|
4502
|
+
if (conflict_target !== void 0) query.conflict_target = conflict_target.join(",");
|
|
4503
|
+
if (return_minimal) query.return = "minimal";
|
|
4504
|
+
const result = await adminFetch(apiClient, {
|
|
3850
4505
|
method: "POST",
|
|
3851
4506
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
3852
4507
|
body: row,
|
|
4508
|
+
query: Object.keys(query).length > 0 ? query : void 0,
|
|
4509
|
+
bearer: pat
|
|
4510
|
+
});
|
|
4511
|
+
const warning = catalogShapeWarning(row);
|
|
4512
|
+
const base = passthroughResult(result);
|
|
4513
|
+
if (warning) base.content.unshift({
|
|
4514
|
+
type: "text",
|
|
4515
|
+
text: warning
|
|
4516
|
+
});
|
|
4517
|
+
return base;
|
|
4518
|
+
});
|
|
4519
|
+
registerTool(server, apiClient, "amba_admin_insert_rows", [
|
|
4520
|
+
"Bulk-insert up to 500 rows into a collection in a single atomic statement — BYPASSES auto-RLS.",
|
|
4521
|
+
"Use for developer-seeded data (question banks, content libraries, lookup tables); far faster than calling amba_admin_insert_row in a loop.",
|
|
4522
|
+
"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.",
|
|
4523
|
+
"on_conflict \"error\" (default) aborts the whole batch on any conflict; \"skip\" emits ON CONFLICT DO NOTHING and reports how many rows were skipped.",
|
|
4524
|
+
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4525
|
+
"Errors: 400 BATCH_TOO_LARGE (>500) / INVALID_COLUMN / NOT_NULL_VIOLATION, 404 COLLECTION_NOT_FOUND, 409 CONFLICT (on_conflict:\"error\")."
|
|
4526
|
+
].join(" "), {
|
|
4527
|
+
project_id: z.string().describe("The project ID."),
|
|
4528
|
+
name: z.string().describe("Collection name."),
|
|
4529
|
+
rows: z.array(z.record(z.unknown())).min(1).max(500).describe("1–500 column-value maps. Server-managed columns are stripped per row."),
|
|
4530
|
+
on_conflict: z.enum(["error", "skip"]).optional().describe("Conflict policy. \"error\" (default) fails the batch; \"skip\" ignores conflicts."),
|
|
4531
|
+
return_minimal: z.boolean().optional().describe("When true, returns only the inserted row ids instead of the full rows. Strongly recommended for large batches or rows with big columns — echoing 500 full rows can exhaust the token budget.")
|
|
4532
|
+
}, async ({ project_id, name, rows, on_conflict, return_minimal }, { pat }) => {
|
|
4533
|
+
const body = { rows };
|
|
4534
|
+
if (on_conflict !== void 0) body.on_conflict = on_conflict;
|
|
4535
|
+
return passthroughResult(await adminFetch(apiClient, {
|
|
4536
|
+
method: "POST",
|
|
4537
|
+
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows:batch`,
|
|
4538
|
+
body,
|
|
4539
|
+
query: return_minimal ? { return: "minimal" } : void 0,
|
|
3853
4540
|
bearer: pat
|
|
3854
4541
|
}));
|
|
3855
4542
|
});
|
|
3856
4543
|
registerTool(server, apiClient, "amba_admin_list_rows", [
|
|
3857
4544
|
"List rows in a collection from the admin side. BYPASSES auto-RLS — every row in the collection is visible.",
|
|
3858
|
-
"Default behavior
|
|
4545
|
+
"Default behavior EXCLUDES soft-deleted rows; pass `include_deleted=true` to include them (e.g. debugging \"where did the row go\").",
|
|
3859
4546
|
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
3860
4547
|
"Supports the server FindQuery DSL via `where` + `order` + `limit` + `offset` + `cursor` + `select`."
|
|
3861
4548
|
].join(" "), {
|
|
@@ -3867,7 +4554,7 @@ function registerTools$6(server, apiClient) {
|
|
|
3867
4554
|
offset: z.number().optional().describe("Offset for pagination."),
|
|
3868
4555
|
cursor: z.string().optional().describe("Opaque cursor returned by a prior page."),
|
|
3869
4556
|
select: z.array(z.string()).optional().describe("Subset of columns to return."),
|
|
3870
|
-
include_deleted: z.boolean().optional().describe("Whether to include soft-deleted rows. Admin default is
|
|
4557
|
+
include_deleted: z.boolean().optional().describe("Whether to include soft-deleted rows. Admin default is FALSE.")
|
|
3871
4558
|
}, async ({ project_id, name, where, order, limit, offset, cursor, select, include_deleted }, { pat }) => {
|
|
3872
4559
|
const findQuery = {};
|
|
3873
4560
|
if (where !== void 0) findQuery.where = where;
|
|
@@ -3884,21 +4571,67 @@ function registerTools$6(server, apiClient) {
|
|
|
3884
4571
|
bearer: pat
|
|
3885
4572
|
}));
|
|
3886
4573
|
});
|
|
4574
|
+
registerTool(server, apiClient, "amba_admin_aggregate_rows", [
|
|
4575
|
+
"Run a group-by aggregation over a collection from the admin side — BYPASSES auto-RLS.",
|
|
4576
|
+
"Supports count / sum / avg / min / max, optionally grouped by one or more columns. count without a column = COUNT(*).",
|
|
4577
|
+
"Default EXCLUDES soft-deleted rows; pass include_deleted=true to include them.",
|
|
4578
|
+
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4579
|
+
"Returns one row per group (or a single row when group_by is omitted), with each aggregation under its alias."
|
|
4580
|
+
].join(" "), {
|
|
4581
|
+
project_id: z.string().describe("The project ID."),
|
|
4582
|
+
name: z.string().describe("Collection name."),
|
|
4583
|
+
select: z.array(z.object({
|
|
4584
|
+
fn: z.enum([
|
|
4585
|
+
"count",
|
|
4586
|
+
"sum",
|
|
4587
|
+
"avg",
|
|
4588
|
+
"min",
|
|
4589
|
+
"max"
|
|
4590
|
+
]),
|
|
4591
|
+
column: z.string().optional().describe("Column to aggregate. Required for sum/avg/min/max; omit for COUNT(*)."),
|
|
4592
|
+
as: z.string().optional().describe("Output alias (defaults to fn or fn_column).")
|
|
4593
|
+
})).min(1).describe("One or more aggregations to compute."),
|
|
4594
|
+
group_by: z.union([z.string(), z.array(z.string())]).optional().describe("Column(s) to group by. Omit for a whole-collection aggregate."),
|
|
4595
|
+
where: adminFindWhereSchema.optional(),
|
|
4596
|
+
include_deleted: z.boolean().optional().describe("Include soft-deleted rows. Default FALSE.")
|
|
4597
|
+
}, async ({ project_id, name, select, group_by, where, include_deleted }, { pat }) => {
|
|
4598
|
+
const body = { select };
|
|
4599
|
+
if (group_by !== void 0) body.group_by = group_by;
|
|
4600
|
+
if (where !== void 0) body.where = where;
|
|
4601
|
+
if (include_deleted !== void 0) body.includeDeleted = include_deleted;
|
|
4602
|
+
return passthroughResult(await adminFetch(apiClient, {
|
|
4603
|
+
method: "POST",
|
|
4604
|
+
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows/aggregate`,
|
|
4605
|
+
body,
|
|
4606
|
+
bearer: pat
|
|
4607
|
+
}));
|
|
4608
|
+
});
|
|
3887
4609
|
server.tool("amba_client_insert_row", [
|
|
3888
4610
|
"Insert a row into a collection as the signed-in app_user. Auto-RLS injects `user_id = <session app_user>` server-side; the body cannot override it.",
|
|
4611
|
+
"Atomic upsert: pass on_conflict=\"ignore\" (return existing row, HTTP 200) or \"update\" (merge) plus conflict_target (unique-index columns, may include user_id). Removes read-then-write races (duplicate profiles, split rooms, etc.).",
|
|
3889
4612
|
"Authenticates as the end-user — pass `api_key` (project client X-Api-Key) plus `session_token` (the app_user's Bearer).",
|
|
3890
|
-
"Errors: 400 INVALID_COLUMN, 401 if api_key/session missing, 404 COLLECTION_NOT_FOUND."
|
|
4613
|
+
"Errors: 400 INVALID_COLUMN / INVALID_CONFLICT_TARGET, 401 if api_key/session missing, 404 COLLECTION_NOT_FOUND."
|
|
3891
4614
|
].join(" "), {
|
|
3892
4615
|
name: z.string().describe("Collection name."),
|
|
3893
4616
|
row: z.record(z.unknown()).describe("Column-value map. Server-managed columns and user_id are stripped/overridden."),
|
|
4617
|
+
on_conflict: z.enum([
|
|
4618
|
+
"error",
|
|
4619
|
+
"ignore",
|
|
4620
|
+
"update"
|
|
4621
|
+
]).optional().describe("Conflict policy. \"error\" (default), \"ignore\" (return existing unchanged), or \"update\" (merge). Requires conflict_target."),
|
|
4622
|
+
conflict_target: z.array(z.string()).optional().describe("Unique-index columns to conflict on (e.g. [\"user_id\",\"kind\"]). Required for ignore/update."),
|
|
3894
4623
|
api_key: z.string().describe("Project client X-Api-Key. End-user tools authenticate via this + an optional session token, NOT via a developer PAT."),
|
|
3895
4624
|
session_token: z.string().optional().describe("Optional app_user session Bearer token. Required by the route's clientSessionAuth.")
|
|
3896
|
-
}, async ({ name, row, api_key, session_token }) => {
|
|
4625
|
+
}, async ({ name, row, on_conflict, conflict_target, api_key, session_token }) => {
|
|
3897
4626
|
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
4627
|
+
const query = {};
|
|
4628
|
+
if (on_conflict !== void 0) query.on_conflict = on_conflict;
|
|
4629
|
+
if (conflict_target !== void 0) query.conflict_target = conflict_target.join(",");
|
|
3898
4630
|
return passthroughResult(await clientFetch(apiClient, {
|
|
3899
4631
|
method: "POST",
|
|
3900
4632
|
path: `/collections/${encodeURIComponent(name)}`,
|
|
3901
4633
|
body: row,
|
|
4634
|
+
...Object.keys(query).length > 0 && { query },
|
|
3902
4635
|
apiKey: api_key,
|
|
3903
4636
|
...session_token !== void 0 && { sessionToken: session_token }
|
|
3904
4637
|
}));
|
|
@@ -3956,6 +4689,7 @@ function registerTools$6(server, apiClient) {
|
|
|
3956
4689
|
});
|
|
3957
4690
|
server.tool("amba_client_update_row", [
|
|
3958
4691
|
"Update a single row by id. Auto-RLS — only succeeds if the row belongs to the signed-in app_user.",
|
|
4692
|
+
"Compare-and-set: pass `expected` (a column→value map) on a single-row update; the write applies only if the row still matches, else 409 PRECONDITION_FAILED. Null-safe. Optimistic concurrency without a transaction.",
|
|
3959
4693
|
"For bulk updates with a where-clause, omit `id` and pass `where` + `set`.",
|
|
3960
4694
|
"Server-managed columns and user_id are rejected with 400 INVALID_COLUMN.",
|
|
3961
4695
|
"Authenticates as the end-user — pass `api_key` + `session_token`."
|
|
@@ -3964,15 +4698,19 @@ function registerTools$6(server, apiClient) {
|
|
|
3964
4698
|
id: z.string().optional().describe("Row UUID for single-row update. Omit to bulk update via `where`."),
|
|
3965
4699
|
where: adminFindWhereSchema.optional().describe("Required for bulk update (when `id` is omitted). `where: {}` matches all rows the user owns."),
|
|
3966
4700
|
set: setSchema.describe("Column-value map to write. Required."),
|
|
4701
|
+
expected: z.record(z.unknown()).optional().describe("Compare-and-set precondition (single-row only): column→expected-value map. The update applies only if the row currently matches; otherwise 409 PRECONDITION_FAILED."),
|
|
3967
4702
|
limit: z.number().optional().describe("Bulk-update cap (default 1000, max 10000). Ignored when `id` is set."),
|
|
3968
4703
|
api_key: z.string().describe("Project client X-Api-Key."),
|
|
3969
4704
|
session_token: z.string().optional().describe("App_user session Bearer. Required by the route.")
|
|
3970
|
-
}, async ({ name, id, where, set, limit, api_key, session_token }) => {
|
|
4705
|
+
}, async ({ name, id, where, set, expected, limit, api_key, session_token }) => {
|
|
3971
4706
|
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
3972
4707
|
if (id !== void 0) return passthroughResult(await clientFetch(apiClient, {
|
|
3973
4708
|
method: "PATCH",
|
|
3974
4709
|
path: `/collections/${encodeURIComponent(name)}/${encodeURIComponent(id)}`,
|
|
3975
|
-
body:
|
|
4710
|
+
body: expected !== void 0 ? {
|
|
4711
|
+
set,
|
|
4712
|
+
expected
|
|
4713
|
+
} : { set },
|
|
3976
4714
|
apiKey: api_key,
|
|
3977
4715
|
...session_token !== void 0 && { sessionToken: session_token }
|
|
3978
4716
|
}));
|
|
@@ -4043,24 +4781,32 @@ function registerTools$6(server, apiClient) {
|
|
|
4043
4781
|
});
|
|
4044
4782
|
server.tool("amba_client_find_rows", [
|
|
4045
4783
|
"Query rows with the @layers/amba SDK-shaped filter body. POSTs to `/v1/client/collections/:name/find` — preferred over `amba_client_list_rows` for anything beyond a flat scan because GET-with-body is fragile across HTTP intermediaries.",
|
|
4046
|
-
"Filter shape: leaf `{column, op, value}` (op ∈ eq|ne|gt|gte|lt|lte|in|not_in|like|ilike|is_null|is_not_null) or combinator `{and: [...]}` / `{or: [...]}` / `{not: {...}}`.",
|
|
4047
|
-
"
|
|
4784
|
+
"Filter shape: leaf `{column, op, value}` (op ∈ eq|ne|gt|gte|lt|lte|in|not_in|like|ilike|is_null|is_not_null|contains|contained_by|overlaps) or combinator `{and: [...]}` / `{or: [...]}` / `{not: {...}}`.",
|
|
4785
|
+
"Keyword/fuzzy search: pass `search: { q, columns, fuzzy?, threshold? }` for classical catalog search across text columns (title/author/tags). Substring (ILIKE) by default; `fuzzy: true` is typo-tolerant and ranks by relevance.",
|
|
4786
|
+
"Auto-RLS — every match is AND'd with `user_id = <session app_user> AND deleted_at IS NULL` server-side (shared/unowned rows are also returned).",
|
|
4048
4787
|
"Authenticates as the end-user — pass `api_key` + `session_token`."
|
|
4049
4788
|
].join(" "), {
|
|
4050
4789
|
name: z.string().describe("Collection name."),
|
|
4051
4790
|
filter: sdkFilterSchema.optional(),
|
|
4052
4791
|
order: sdkOrderSchema.optional(),
|
|
4792
|
+
search: z.object({
|
|
4793
|
+
q: z.string().describe("Search text."),
|
|
4794
|
+
columns: z.array(z.string()).min(1).describe("Text columns to search."),
|
|
4795
|
+
fuzzy: z.boolean().optional().describe("Typo-tolerant trigram match + relevance ranking. Default false (substring)."),
|
|
4796
|
+
threshold: z.number().optional().describe("Fuzzy only: similarity threshold 0..1 (default 0.3).")
|
|
4797
|
+
}).optional().describe("Keyword / fuzzy text search across columns."),
|
|
4053
4798
|
limit: z.number().optional().describe("Max rows."),
|
|
4054
4799
|
cursor: z.string().optional().describe("Opaque cursor returned by a prior page."),
|
|
4055
4800
|
select: z.array(z.string()).optional().describe("Subset of columns to return."),
|
|
4056
4801
|
include_deleted: z.boolean().optional().describe("Include soft-deleted rows. Default FALSE."),
|
|
4057
4802
|
api_key: z.string().describe("Project client X-Api-Key."),
|
|
4058
4803
|
session_token: z.string().optional().describe("App_user session Bearer. Required by the route.")
|
|
4059
|
-
}, async ({ name, filter, order, limit, cursor, select, include_deleted, api_key, session_token }) => {
|
|
4804
|
+
}, async ({ name, filter, order, search, limit, cursor, select, include_deleted, api_key, session_token }) => {
|
|
4060
4805
|
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
4061
4806
|
const body = {};
|
|
4062
4807
|
if (filter !== void 0) body.filter = filter;
|
|
4063
4808
|
if (order !== void 0) body.order = order;
|
|
4809
|
+
if (search !== void 0) body.search = search;
|
|
4064
4810
|
if (limit !== void 0) body.limit = limit;
|
|
4065
4811
|
if (cursor !== void 0) body.cursor = cursor;
|
|
4066
4812
|
if (select !== void 0) body.select = select;
|
|
@@ -4232,24 +4978,27 @@ function jsonResult(payload) {
|
|
|
4232
4978
|
* of a generic upstream 400.
|
|
4233
4979
|
*/
|
|
4234
4980
|
const MAX_BUNDLE_BYTES = 10 * 1024 * 1024;
|
|
4235
|
-
function registerTools$
|
|
4981
|
+
function registerTools$11(server, apiClient) {
|
|
4236
4982
|
registerTool(server, apiClient, "amba_functions_deploy", [
|
|
4237
4983
|
"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
4984
|
"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).",
|
|
4239
4985
|
"Optional `rate_limit` declares a per-function rate-limit config (validated server-side). Pass null/omit for no rate limit.",
|
|
4986
|
+
"Optional `public` (default false) deploys the function WITHOUT the X-Api-Key gate, so third-party webhook senders (RevenueCat, Stripe, GitHub, Slack) that cannot attach a custom header can reach it. SECURITY: when public, YOU must verify the webhook signature (HMAC) inside the function body — the runtime no longer requires a caller credential. `public: true` only skips the key requirement; rate-limiting and the per-request signed identity context still apply. Leave it false for functions only your own app calls.",
|
|
4240
4987
|
"Returns the function deployment row + the public URL (`fn_url`: `https://{project_slug}.fn.amba.host/{name}`)."
|
|
4241
4988
|
].join(" "), {
|
|
4242
4989
|
project_id: z.string().describe("The Amba project ID."),
|
|
4243
4990
|
name: z.string().describe("Function name. Lowercase, /^[a-z][a-z0-9_-]*$/, ≤58 chars."),
|
|
4244
4991
|
code: z.string().describe("Bundled JavaScript module source (one entry file)."),
|
|
4245
|
-
rate_limit: z.unknown().optional().describe("Optional rate-limit config object. Shape: see @layers/amba-shared:RateLimitConfig.")
|
|
4246
|
-
|
|
4992
|
+
rate_limit: z.unknown().optional().describe("Optional rate-limit config object. Shape: see @layers/amba-shared:RateLimitConfig."),
|
|
4993
|
+
public: z.boolean().optional().describe("Deploy the function as PUBLIC (no X-Api-Key required) so external webhook senders can reach it. Default false. When true, you MUST verify the webhook HMAC signature inside the function body — public skips the credential gate only; rate-limiting and signed identity context still apply.")
|
|
4994
|
+
}, async ({ project_id, name, code, rate_limit, public: isPublic }, { pat }) => {
|
|
4247
4995
|
const byteLen = Buffer.byteLength(code, "utf8");
|
|
4248
4996
|
if (byteLen > MAX_BUNDLE_BYTES) throw new Error(`Bundle size ${byteLen} bytes exceeds ${MAX_BUNDLE_BYTES}-byte cap (10 MiB).`);
|
|
4249
4997
|
const form = new FormData();
|
|
4250
4998
|
form.append("script", new Blob([code], { type: "application/javascript" }), "bundle.js");
|
|
4251
4999
|
const metadata = { name };
|
|
4252
5000
|
if (rate_limit !== void 0 && rate_limit !== null) metadata.rate_limit = rate_limit;
|
|
5001
|
+
if (isPublic === true) metadata.public = true;
|
|
4253
5002
|
form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }), "metadata.json");
|
|
4254
5003
|
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/functions/deploy`, { formData: form }));
|
|
4255
5004
|
}, ["amba_deploy_function"]);
|
|
@@ -4357,7 +5106,7 @@ function registerTools$5(server, apiClient) {
|
|
|
4357
5106
|
}
|
|
4358
5107
|
//#endregion
|
|
4359
5108
|
//#region src/tools/sites.ts
|
|
4360
|
-
function registerTools$
|
|
5109
|
+
function registerTools$10(server, apiClient) {
|
|
4361
5110
|
registerTool(server, apiClient, "amba_sites_deploy", [
|
|
4362
5111
|
"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
5112
|
"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 +5197,81 @@ function registerTools$4(server, apiClient) {
|
|
|
4448
5197
|
}, ["amba_remove_site_domain"]);
|
|
4449
5198
|
}
|
|
4450
5199
|
//#endregion
|
|
5200
|
+
//#region src/tools/domains.ts
|
|
5201
|
+
function registerTools$9(server, apiClient) {
|
|
5202
|
+
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(" "), {
|
|
5203
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5204
|
+
query: z.string().describe("A keyword (e.g. \"getunbury\") or a full domain (e.g. \"unbury.com\")."),
|
|
5205
|
+
limit: z.number().int().min(1).max(50).optional().describe("Max suggestions (1-50, default 20).")
|
|
5206
|
+
}, async ({ project_id, query, limit }, { pat }) => {
|
|
5207
|
+
const q = { q: query };
|
|
5208
|
+
if (limit !== void 0) q.limit = String(limit);
|
|
5209
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/domains/search`, { query: q }));
|
|
5210
|
+
});
|
|
5211
|
+
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(" "), {
|
|
5212
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5213
|
+
domains: z.array(z.string()).min(1).max(20).describe("Domains to check, e.g. [\"unbury.com\", \"unbury.app\"]. Lowercased at the boundary.")
|
|
5214
|
+
}, async ({ project_id, domains }, { pat }) => {
|
|
5215
|
+
const normalised = domains.map((d) => d.toLowerCase());
|
|
5216
|
+
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/domains/check`, { body: { domains: normalised } }));
|
|
5217
|
+
});
|
|
5218
|
+
registerTool(server, apiClient, "amba_domains_purchase", [
|
|
5219
|
+
"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.",
|
|
5220
|
+
"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.",
|
|
5221
|
+
"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.",
|
|
5222
|
+
"On success: the domain is registered, DNS is auto-configured, and the site begins serving on the new domain (the TLS certificate issues automatically)."
|
|
5223
|
+
].join(" "), {
|
|
5224
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5225
|
+
domain: z.string().describe("The domain to buy, e.g. \"unbury.com\". Must be available (check first)."),
|
|
5226
|
+
site: z.string().describe("The site name to connect the domain to (from amba_sites_list)."),
|
|
5227
|
+
accept_price_usd: z.number().optional().describe("Required to execute: the price (USD) you were quoted. Omit on the first (quote) call."),
|
|
5228
|
+
confirm: z.boolean().optional().describe("Set true ONLY after the user has approved the cost. Omit on the first (quote) call."),
|
|
5229
|
+
auto_renew: z.boolean().optional().describe("Auto-renew the domain each year (default true)."),
|
|
5230
|
+
privacy: z.boolean().optional().describe("Enable WHOIS privacy (default true)."),
|
|
5231
|
+
years: z.number().int().min(1).max(10).optional().describe("Registration length in years (default 1).")
|
|
5232
|
+
}, async ({ project_id, domain, site, accept_price_usd, confirm, auto_renew, privacy, years }, { pat }) => {
|
|
5233
|
+
const body = {
|
|
5234
|
+
domain: domain.toLowerCase(),
|
|
5235
|
+
site
|
|
5236
|
+
};
|
|
5237
|
+
if (accept_price_usd !== void 0) body.accept_price_usd = accept_price_usd;
|
|
5238
|
+
if (confirm !== void 0) body.confirm = confirm;
|
|
5239
|
+
if (auto_renew !== void 0) body.auto_renew = auto_renew;
|
|
5240
|
+
if (privacy !== void 0) body.privacy = privacy;
|
|
5241
|
+
if (years !== void 0) body.years = years;
|
|
5242
|
+
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/domains/purchase`, { body }));
|
|
5243
|
+
});
|
|
5244
|
+
registerTool(server, apiClient, "amba_domains_list", "List domains this project has purchased through Amba, with registration status and the site each is connected to.", {
|
|
5245
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5246
|
+
limit: z.number().int().min(1).max(200).optional().describe("Page size (1-200, default 50)."),
|
|
5247
|
+
offset: z.number().int().min(0).optional().describe("Skip rows (default 0).")
|
|
5248
|
+
}, async ({ project_id, limit, offset }, { pat }) => {
|
|
5249
|
+
const query = {};
|
|
5250
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
5251
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
5252
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/domains`, { query }));
|
|
5253
|
+
});
|
|
5254
|
+
}
|
|
5255
|
+
//#endregion
|
|
4451
5256
|
//#region src/tools/secrets.ts
|
|
4452
|
-
function registerTools$
|
|
5257
|
+
function registerTools$8(server, apiClient) {
|
|
4453
5258
|
registerTool(server, apiClient, "amba_secrets_set", [
|
|
4454
|
-
"Set or rotate a function-
|
|
4455
|
-
"Writes a new version to the backing secret store and enqueues
|
|
4456
|
-
"Secret name must match /^[A-Z][A-Z0-9_]{0,62}$/. Function name must match /^[a-z][a-z0-9_-]{0,57}$/.",
|
|
5259
|
+
"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.",
|
|
5260
|
+
"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).",
|
|
5261
|
+
"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
5262
|
"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
|
-
"
|
|
5263
|
+
"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
5264
|
"Value cap: 64 KiB. Plaintext is never returned afterwards."
|
|
4460
5265
|
].join(" "), {
|
|
4461
5266
|
project_id: z.string().describe("The Amba project ID."),
|
|
4462
5267
|
name: z.string().describe("Secret name. UPPER_SNAKE_CASE. /^[A-Z][A-Z0-9_]{0,62}$/."),
|
|
4463
5268
|
value: z.string().describe("Secret value. Stored encrypted at rest; never returned in plaintext afterwards."),
|
|
4464
|
-
function: z.string().describe("Function
|
|
5269
|
+
function: z.string().optional().describe("Function this secret binds to. OMIT for a project-wide secret (applied to every function in the project).")
|
|
4465
5270
|
}, async ({ project_id, name, value, function: functionName }, { pat }) => {
|
|
4466
|
-
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/secrets`, { body: {
|
|
5271
|
+
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/secrets`, { body: functionName === void 0 ? {
|
|
5272
|
+
name,
|
|
5273
|
+
value
|
|
5274
|
+
} : {
|
|
4467
5275
|
name,
|
|
4468
5276
|
value,
|
|
4469
5277
|
function: functionName
|
|
@@ -4488,25 +5296,26 @@ function registerTools$3(server, apiClient) {
|
|
|
4488
5296
|
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/secrets`));
|
|
4489
5297
|
}, ["amba_list_secrets"]);
|
|
4490
5298
|
registerTool(server, apiClient, "amba_secrets_delete", [
|
|
4491
|
-
"Delete a
|
|
4492
|
-
"
|
|
5299
|
+
"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).",
|
|
5300
|
+
"Pass `function` to delete a function-scoped secret; OMIT it to delete the project-wide secret of this name.",
|
|
4493
5301
|
"Idempotent: deleting a secret that no longer exists succeeds."
|
|
4494
5302
|
].join(" "), {
|
|
4495
5303
|
project_id: z.string().describe("The Amba project ID."),
|
|
4496
5304
|
name: z.string().describe("Secret name. /^[A-Z][A-Z0-9_]{0,62}$/."),
|
|
4497
|
-
function: z.string().describe("Function
|
|
5305
|
+
function: z.string().optional().describe("Function this secret was bound to. OMIT to delete the project-wide secret.")
|
|
4498
5306
|
}, async ({ project_id, name, function: functionName }, { pat }) => {
|
|
5307
|
+
const scope = functionName ?? null;
|
|
4499
5308
|
try {
|
|
4500
|
-
return jsonResult(await callWithPat(apiClient, pat, "DELETE", `/projects/${encodeURIComponent(project_id)}/secrets/${encodeURIComponent(name)}`, { query: { function: functionName } }) ?? {
|
|
5309
|
+
return jsonResult(await callWithPat(apiClient, pat, "DELETE", `/projects/${encodeURIComponent(project_id)}/secrets/${encodeURIComponent(name)}`, functionName === void 0 ? {} : { query: { function: functionName } }) ?? {
|
|
4501
5310
|
deleted: true,
|
|
4502
5311
|
name,
|
|
4503
|
-
function:
|
|
5312
|
+
function: scope
|
|
4504
5313
|
});
|
|
4505
5314
|
} catch (err) {
|
|
4506
5315
|
if (err instanceof AmbaApiError && err.status === 404) return jsonResult({
|
|
4507
5316
|
deleted: true,
|
|
4508
5317
|
name,
|
|
4509
|
-
function:
|
|
5318
|
+
function: scope,
|
|
4510
5319
|
already_absent: true
|
|
4511
5320
|
});
|
|
4512
5321
|
throw err;
|
|
@@ -4515,22 +5324,26 @@ function registerTools$3(server, apiClient) {
|
|
|
4515
5324
|
}
|
|
4516
5325
|
//#endregion
|
|
4517
5326
|
//#region src/tools/ai-prompts-admin.ts
|
|
4518
|
-
const PROVIDER_ENUM = z.enum([
|
|
5327
|
+
const PROVIDER_ENUM$1 = z.enum([
|
|
5328
|
+
"anthropic",
|
|
5329
|
+
"openai",
|
|
5330
|
+
"mistral"
|
|
5331
|
+
]);
|
|
4519
5332
|
const RATE_LIMIT_SHAPE = z.object({
|
|
4520
5333
|
window: z.string().optional().describe("Window length (e.g. \"60s\", \"1m\", \"1h\"). Defaults to \"60s\"."),
|
|
4521
5334
|
max: z.number().int().min(1).optional().describe("Max invocations per window. Defaults to 20."),
|
|
4522
5335
|
key: z.string().optional().describe("Bucketing key. Typically \"user_id\" or \"session_id\". Defaults to \"user_id\".")
|
|
4523
5336
|
}).strict();
|
|
4524
|
-
function registerTools$
|
|
5337
|
+
function registerTools$7(server, apiClient) {
|
|
4525
5338
|
registerTool(server, apiClient, "amba_ai_prompts_create", [
|
|
4526
5339
|
"Register a new AI prompt template on a project. Returns the persisted prompt row (name, version, provider, model, client_invokable).",
|
|
4527
|
-
"Name must match /^[a-z][a-z0-9_-]{0,127}$/. Provider must be \"anthropic\" or \"
|
|
5340
|
+
"Name must match /^[a-z][a-z0-9_-]{0,127}$/. Provider must be \"anthropic\", \"openai\", or \"mistral\" and must already be registered via `amba_ai_providers_set`. Model is the provider-native model id (e.g. \"claude-sonnet-4-5-20250929\", \"gpt-4o-2024-08-06\").",
|
|
4528
5341
|
"Set `client_invokable=true` to allow SDK callers (e.g. amba-web, amba-react) to invoke the prompt via `Amba.ai.run()`. Defaults false — server-side / admin invocation only.",
|
|
4529
5342
|
"Re-issuing this tool with an existing `name` will bump the prompt to a new version; prefer `amba_ai_prompts_update` for that intent."
|
|
4530
5343
|
].join(" "), {
|
|
4531
5344
|
project_id: z.string().describe("The Amba project ID."),
|
|
4532
5345
|
name: z.string().describe("Prompt name. /^[a-z][a-z0-9_-]{0,127}$/."),
|
|
4533
|
-
provider: PROVIDER_ENUM.describe("AI provider. Must already be registered
|
|
5346
|
+
provider: PROVIDER_ENUM$1.describe("AI provider. Must already be registered via `amba_ai_providers_set`."),
|
|
4534
5347
|
model: z.string().describe("Provider-native model id."),
|
|
4535
5348
|
system_prompt: z.string().nullable().optional().describe("System prompt sent on every invocation. Omit or null for no system prompt."),
|
|
4536
5349
|
max_tokens: z.number().int().min(1).max(2e5).optional().describe("Max output tokens per invocation. Defaults to 4096."),
|
|
@@ -4560,7 +5373,7 @@ function registerTools$2(server, apiClient) {
|
|
|
4560
5373
|
registerTool(server, apiClient, "amba_ai_prompts_update", ["Update an existing AI prompt template. The new version replaces the old (incrementing `version`). Every field you supply overwrites the prior value; the API replaces ALL fields on every upsert, so always pass the full intended state.", "Returns the persisted row (with the bumped version)."].join(" "), {
|
|
4561
5374
|
project_id: z.string().describe("The Amba project ID."),
|
|
4562
5375
|
name: z.string().describe("Prompt name to update."),
|
|
4563
|
-
provider: PROVIDER_ENUM.describe("AI provider."),
|
|
5376
|
+
provider: PROVIDER_ENUM$1.describe("AI provider."),
|
|
4564
5377
|
model: z.string().describe("Provider-native model id."),
|
|
4565
5378
|
system_prompt: z.string().nullable().optional().describe("System prompt; null/omit to clear."),
|
|
4566
5379
|
max_tokens: z.number().int().min(1).max(2e5).optional().describe("Max output tokens."),
|
|
@@ -4640,6 +5453,179 @@ function registerTools$2(server, apiClient) {
|
|
|
4640
5453
|
}, ["amba_invoke_ai_prompt"]);
|
|
4641
5454
|
}
|
|
4642
5455
|
//#endregion
|
|
5456
|
+
//#region src/tools/ai-providers-admin.ts
|
|
5457
|
+
const PROVIDER_ENUM = z.enum([
|
|
5458
|
+
"anthropic",
|
|
5459
|
+
"openai",
|
|
5460
|
+
"mistral",
|
|
5461
|
+
"gemini"
|
|
5462
|
+
]);
|
|
5463
|
+
function registerTools$6(server, apiClient) {
|
|
5464
|
+
registerTool(server, apiClient, "amba_ai_providers_set", [
|
|
5465
|
+
"Register or rotate the upstream AI provider API key for a project. This is the key the AI gateway uses to call the model on your behalf when you invoke a prompt or `Amba.ai.*`.",
|
|
5466
|
+
"Call this BEFORE `amba_ai_prompts_create` — a prompt references a registered provider, and invocations fail with `provider_not_configured` (424) until a key is set.",
|
|
5467
|
+
"This is NOT `amba_secrets_set`: that writes function-scoped Worker secrets, which the AI gateway never reads. The provider key lives in a separate gateway-owned namespace and is set only here.",
|
|
5468
|
+
"The key is stored server-side; it is never returned. The response includes a short masked preview (`api_key_preview`) so you can confirm which key is active."
|
|
5469
|
+
].join(" "), {
|
|
5470
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5471
|
+
provider: PROVIDER_ENUM.describe("Provider the key belongs to: \"anthropic\", \"openai\", \"mistral\", or \"gemini\"."),
|
|
5472
|
+
api_key: z.string().min(10).describe("The upstream provider API key (e.g. an Anthropic or OpenAI account key).")
|
|
5473
|
+
}, async ({ project_id, provider, api_key }, { pat }) => {
|
|
5474
|
+
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/ai/providers`, { body: {
|
|
5475
|
+
name: provider,
|
|
5476
|
+
api_key
|
|
5477
|
+
} }));
|
|
5478
|
+
});
|
|
5479
|
+
registerTool(server, apiClient, "amba_ai_providers_list", "List the AI providers registered on a project. Returns each provider with a `configured` boolean (true once a key is set) and timestamps. Keys themselves are never returned.", { project_id: z.string().describe("The Amba project ID.") }, async ({ project_id }, { pat }) => {
|
|
5480
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/ai/providers`));
|
|
5481
|
+
});
|
|
5482
|
+
registerTool(server, apiClient, "amba_ai_providers_delete", "Revoke a registered AI provider key. Rejected with 409 PROVIDER_HAS_PROMPTS if any prompt still references the provider — delete those prompts first.", {
|
|
5483
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5484
|
+
provider: PROVIDER_ENUM.describe("Provider to revoke.")
|
|
5485
|
+
}, async ({ project_id, provider }, { pat }) => {
|
|
5486
|
+
return jsonResult(await callWithPat(apiClient, pat, "DELETE", `/projects/${encodeURIComponent(project_id)}/ai/providers/${encodeURIComponent(provider)}`) ?? {
|
|
5487
|
+
name: provider,
|
|
5488
|
+
deleted: true
|
|
5489
|
+
});
|
|
5490
|
+
});
|
|
5491
|
+
}
|
|
5492
|
+
//#endregion
|
|
5493
|
+
//#region src/tools/experiments.ts
|
|
5494
|
+
const variantSchema = z.object({
|
|
5495
|
+
key: z.string().describe("Variant key (e.g. \"control\", \"treatment\"). Unique within the experiment."),
|
|
5496
|
+
weight: z.number().int().min(1).max(1e6).describe("Relative allocation weight (positive integer). Weights need not sum to 100.")
|
|
5497
|
+
});
|
|
5498
|
+
const STATUS_ENUM = z.enum([
|
|
5499
|
+
"active",
|
|
5500
|
+
"paused",
|
|
5501
|
+
"ended"
|
|
5502
|
+
]).describe("active = assigns + exposes new users; paused = serves existing assignments but issues no new ones; ended = read-only.");
|
|
5503
|
+
function registerTools$5(server, apiClient) {
|
|
5504
|
+
registerTool(server, apiClient, "amba_experiments_create", [
|
|
5505
|
+
"Create an A/B experiment: a stable `key`, a display `name`, and >= 2 weighted variants.",
|
|
5506
|
+
"End-users are bucketed into a STICKY variant the first time they request an assignment",
|
|
5507
|
+
"(via the SDK) using a deterministic hash over the variant weights; the bucket then never",
|
|
5508
|
+
"changes even if you re-weight later. The FIRST variant is the control for significance."
|
|
5509
|
+
].join(" "), {
|
|
5510
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5511
|
+
key: z.string().describe("Stable experiment key (the SDK addresses the experiment by this)."),
|
|
5512
|
+
name: z.string().describe("Human-readable experiment name."),
|
|
5513
|
+
description: z.string().nullable().optional().describe("Optional description."),
|
|
5514
|
+
variants: z.array(variantSchema).min(2).describe("At least 2 weighted variants. First is the control."),
|
|
5515
|
+
status: STATUS_ENUM.optional().describe("Initial status. Defaults to \"active\".")
|
|
5516
|
+
}, async ({ project_id, key, name, description, variants, status }, { client }) => {
|
|
5517
|
+
const payload = {
|
|
5518
|
+
key,
|
|
5519
|
+
name,
|
|
5520
|
+
variants
|
|
5521
|
+
};
|
|
5522
|
+
if (description !== void 0) payload.description = description;
|
|
5523
|
+
if (status !== void 0) payload.status = status;
|
|
5524
|
+
const result = await client.post(`/projects/${encodeURIComponent(project_id)}/experiments`, payload);
|
|
5525
|
+
return { content: [{
|
|
5526
|
+
type: "text",
|
|
5527
|
+
text: JSON.stringify(result, null, 2)
|
|
5528
|
+
}] };
|
|
5529
|
+
});
|
|
5530
|
+
registerTool(server, apiClient, "amba_experiments_list", "List every experiment on a project (key, name, variants, status, timestamps), ordered by key.", { project_id: z.string().describe("The Amba project ID.") }, async ({ project_id }, { client }) => {
|
|
5531
|
+
const result = await client.get(`/projects/${encodeURIComponent(project_id)}/experiments`);
|
|
5532
|
+
return { content: [{
|
|
5533
|
+
type: "text",
|
|
5534
|
+
text: JSON.stringify(result, null, 2)
|
|
5535
|
+
}] };
|
|
5536
|
+
});
|
|
5537
|
+
registerTool(server, apiClient, "amba_experiments_get", "Get a single experiment by key (name, variants, status, timestamps). 404 if not found.", {
|
|
5538
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5539
|
+
key: z.string().describe("Experiment key.")
|
|
5540
|
+
}, async ({ project_id, key }, { client }) => {
|
|
5541
|
+
const result = await client.get(`/projects/${encodeURIComponent(project_id)}/experiments/${encodeURIComponent(key)}`);
|
|
5542
|
+
return { content: [{
|
|
5543
|
+
type: "text",
|
|
5544
|
+
text: JSON.stringify(result, null, 2)
|
|
5545
|
+
}] };
|
|
5546
|
+
});
|
|
5547
|
+
registerTool(server, apiClient, "amba_experiments_update", [
|
|
5548
|
+
"Update an experiment's name, description, variants, and/or status. Only the supplied",
|
|
5549
|
+
"fields change; variants (when supplied) replace the whole list. Re-weighting NEVER",
|
|
5550
|
+
"re-rolls already-assigned users (assignments are sticky); it only affects users assigned",
|
|
5551
|
+
"after the edit. Set status to \"paused\" to stop new assignments or \"ended\" to freeze it."
|
|
5552
|
+
].join(" "), {
|
|
5553
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5554
|
+
key: z.string().describe("Experiment key to update."),
|
|
5555
|
+
name: z.string().optional().describe("New name."),
|
|
5556
|
+
description: z.string().nullable().optional().describe("New description; null to clear."),
|
|
5557
|
+
variants: z.array(variantSchema).min(2).optional().describe("Replacement variant list (>= 2). First is the control."),
|
|
5558
|
+
status: STATUS_ENUM.optional().describe("New status.")
|
|
5559
|
+
}, async ({ project_id, key, name, description, variants, status }, { client }) => {
|
|
5560
|
+
const payload = {};
|
|
5561
|
+
if (name !== void 0) payload.name = name;
|
|
5562
|
+
if (description !== void 0) payload.description = description;
|
|
5563
|
+
if (variants !== void 0) payload.variants = variants;
|
|
5564
|
+
if (status !== void 0) payload.status = status;
|
|
5565
|
+
const result = await client.patch(`/projects/${encodeURIComponent(project_id)}/experiments/${encodeURIComponent(key)}`, payload);
|
|
5566
|
+
return { content: [{
|
|
5567
|
+
type: "text",
|
|
5568
|
+
text: JSON.stringify(result, null, 2)
|
|
5569
|
+
}] };
|
|
5570
|
+
});
|
|
5571
|
+
registerTool(server, apiClient, "amba_experiments_delete", "Delete an experiment by key. Cascades its assignments + exposures. Hard delete.", {
|
|
5572
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5573
|
+
key: z.string().describe("Experiment key to delete.")
|
|
5574
|
+
}, async ({ project_id, key }, { client }) => {
|
|
5575
|
+
const result = await client.delete(`/projects/${encodeURIComponent(project_id)}/experiments/${encodeURIComponent(key)}`);
|
|
5576
|
+
return { content: [{
|
|
5577
|
+
type: "text",
|
|
5578
|
+
text: JSON.stringify(result ?? {
|
|
5579
|
+
key,
|
|
5580
|
+
deleted: true
|
|
5581
|
+
}, null, 2)
|
|
5582
|
+
}] };
|
|
5583
|
+
});
|
|
5584
|
+
registerTool(server, apiClient, "amba_experiments_results", [
|
|
5585
|
+
"Compute per-variant results for an experiment: assigned_count, exposed_count, and",
|
|
5586
|
+
"(when `conversion_event` is supplied) converted_count + conversion_rate — the distinct",
|
|
5587
|
+
"exposed users who emitted that analytics event at/after their assignment. Each non-control",
|
|
5588
|
+
"variant also gets a two-proportion z-test vs the control (first variant): a z_score and an",
|
|
5589
|
+
"approximate two-sided p_value. The control variant's z_score/p_value are null."
|
|
5590
|
+
].join(" "), {
|
|
5591
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5592
|
+
key: z.string().describe("Experiment key."),
|
|
5593
|
+
conversion_event: z.string().optional().describe("Analytics event_name to treat as the conversion goal. Omit to get assigned/exposed counts only (conversions reported as 0).")
|
|
5594
|
+
}, async ({ project_id, key, conversion_event }, { client }) => {
|
|
5595
|
+
const query = {};
|
|
5596
|
+
if (conversion_event !== void 0) query.conversion_event = conversion_event;
|
|
5597
|
+
const result = await client.get(`/projects/${encodeURIComponent(project_id)}/experiments/${encodeURIComponent(key)}/results`, query);
|
|
5598
|
+
return { content: [{
|
|
5599
|
+
type: "text",
|
|
5600
|
+
text: JSON.stringify(result, null, 2)
|
|
5601
|
+
}] };
|
|
5602
|
+
});
|
|
5603
|
+
}
|
|
5604
|
+
//#endregion
|
|
5605
|
+
//#region src/tools/promotion.ts
|
|
5606
|
+
function registerTools$4(server, apiClient) {
|
|
5607
|
+
registerTool(server, apiClient, "amba_project_export", [
|
|
5608
|
+
"Export a declarative bundle of a project's reusable configuration so it can be re-created in another project (e.g. promote dev → prod) in one operation.",
|
|
5609
|
+
"The bundle includes (each section only when present): remote configs, collection SCHEMAS (column defs + indexes, no rows), content libraries + their items, and the full gamification set — currencies, achievements, streaks, challenges, leaderboards, and xp rules — as DEFINITIONS only.",
|
|
5610
|
+
"It carries NO secrets, NO per-user data, and NO end-user rows. Pass the returned `bundle` to `amba_project_import` against the target project."
|
|
5611
|
+
].join(" "), { project_id: z.string().describe("The Amba project ID to export configuration FROM.") }, async ({ project_id }, { pat }) => {
|
|
5612
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/promotion/export`));
|
|
5613
|
+
});
|
|
5614
|
+
registerTool(server, apiClient, "amba_project_import", [
|
|
5615
|
+
"Import a declarative configuration bundle (from `amba_project_export`) into a project. Idempotently creates remote configs, collections, content libraries + items, and gamification definitions (currencies, achievements, streaks, challenges, leaderboards, xp rules).",
|
|
5616
|
+
"mode=\"skip_existing\" (default) leaves any entity that already exists (by name/key/code) untouched; mode=\"merge\" refreshes the definition of existing entities. Either mode is safe to run twice — re-import never duplicates.",
|
|
5617
|
+
"Returns a per-section summary of { created, skipped, updated }. Collections are create-if-absent only (an existing collection is skipped; alter the schema via the dedicated collection tools)."
|
|
5618
|
+
].join(" "), {
|
|
5619
|
+
project_id: z.string().describe("The Amba project ID to import configuration INTO."),
|
|
5620
|
+
bundle: z.record(z.unknown()).describe("The bundle object returned by `amba_project_export` (its `data` field)."),
|
|
5621
|
+
mode: z.enum(["merge", "skip_existing"]).optional().describe("Conflict resolution. \"skip_existing\" (default) no-ops on a name/key/code conflict; \"merge\" updates the existing definition.")
|
|
5622
|
+
}, async ({ project_id, bundle, mode }, { pat }) => {
|
|
5623
|
+
const body = { bundle };
|
|
5624
|
+
if (mode !== void 0) body.mode = mode;
|
|
5625
|
+
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/promotion/import`, { body }));
|
|
5626
|
+
});
|
|
5627
|
+
}
|
|
5628
|
+
//#endregion
|
|
4643
5629
|
//#region src/tools/billing.ts
|
|
4644
5630
|
/**
|
|
4645
5631
|
* Static tier catalog. Mirrors the marketing-page structure shipped in
|
|
@@ -4723,7 +5709,7 @@ const TIER_CATALOG = {
|
|
|
4723
5709
|
note: "Use Amba.track(name, props, { telemetry: true }) for high-volume telemetry. Cheaper, no fan-out to segments/workflows/push."
|
|
4724
5710
|
}
|
|
4725
5711
|
};
|
|
4726
|
-
function registerTools$
|
|
5712
|
+
function registerTools$3(server, apiClient) {
|
|
4727
5713
|
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
5714
|
const result = await client.get(`/projects/${project_id}/billing/status`);
|
|
4729
5715
|
return { content: [{
|
|
@@ -4750,7 +5736,7 @@ function registerTools$1(server, apiClient) {
|
|
|
4750
5736
|
}
|
|
4751
5737
|
//#endregion
|
|
4752
5738
|
//#region src/tools/invites.ts
|
|
4753
|
-
function registerTools(server, apiClient) {
|
|
5739
|
+
function registerTools$2(server, apiClient) {
|
|
4754
5740
|
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
5741
|
project_id: z.string().describe("The project ID"),
|
|
4756
5742
|
email: z.string().describe("Teammate's email address"),
|
|
@@ -4777,7 +5763,7 @@ function registerTools(server, apiClient) {
|
|
|
4777
5763
|
text: JSON.stringify(result, null, 2)
|
|
4778
5764
|
}] };
|
|
4779
5765
|
});
|
|
4780
|
-
registerTool(server, apiClient, "amba_projects_remove_member", "Remove a member from a project. The owner cannot be removed
|
|
5766
|
+
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
5767
|
project_id: z.string().describe("The project ID"),
|
|
4782
5768
|
developer_id: z.string().describe("The developer ID to remove")
|
|
4783
5769
|
}, async ({ project_id, developer_id }, { client }) => {
|
|
@@ -4787,6 +5773,304 @@ function registerTools(server, apiClient) {
|
|
|
4787
5773
|
text: JSON.stringify(result, null, 2)
|
|
4788
5774
|
}] };
|
|
4789
5775
|
});
|
|
5776
|
+
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.", {
|
|
5777
|
+
project_id: z.string().describe("The project ID"),
|
|
5778
|
+
new_owner_developer_id: z.string().describe("Developer ID of the existing member who should become the new owner.")
|
|
5779
|
+
}, async ({ project_id, new_owner_developer_id }, { client }) => {
|
|
5780
|
+
const result = await client.post(`/projects/${project_id}/transfer-ownership`, { new_owner_developer_id });
|
|
5781
|
+
return { content: [{
|
|
5782
|
+
type: "text",
|
|
5783
|
+
text: JSON.stringify(result, null, 2)
|
|
5784
|
+
}] };
|
|
5785
|
+
});
|
|
5786
|
+
}
|
|
5787
|
+
//#endregion
|
|
5788
|
+
//#region src/tools/webhooks.ts
|
|
5789
|
+
function registerTools$1(server, apiClient) {
|
|
5790
|
+
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).", {
|
|
5791
|
+
project_id: z.string().describe("The project ID"),
|
|
5792
|
+
event_name: z.string().describe("Event name to subscribe to. Must match ^[a-z][a-z0-9_.-]{0,127}$ (case-insensitive)."),
|
|
5793
|
+
target_url: z.string().describe("HTTPS endpoint that receives the signed POST. http:// allowed only for localhost.")
|
|
5794
|
+
}, async ({ project_id, event_name, target_url }, { client }) => {
|
|
5795
|
+
const result = await client.post(`/projects/${project_id}/webhooks/subscriptions`, {
|
|
5796
|
+
event_name,
|
|
5797
|
+
target_url
|
|
5798
|
+
});
|
|
5799
|
+
return { content: [{
|
|
5800
|
+
type: "text",
|
|
5801
|
+
text: JSON.stringify(result, null, 2)
|
|
5802
|
+
}] };
|
|
5803
|
+
});
|
|
5804
|
+
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 }) => {
|
|
5805
|
+
const result = await client.get(`/projects/${project_id}/webhooks/subscriptions`);
|
|
5806
|
+
return { content: [{
|
|
5807
|
+
type: "text",
|
|
5808
|
+
text: JSON.stringify(result, null, 2)
|
|
5809
|
+
}] };
|
|
5810
|
+
});
|
|
5811
|
+
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).", {
|
|
5812
|
+
project_id: z.string().describe("The project ID"),
|
|
5813
|
+
subscription_id: z.string().describe("The subscription UUID")
|
|
5814
|
+
}, async ({ project_id, subscription_id }, { client }) => {
|
|
5815
|
+
const result = await client.get(`/projects/${project_id}/webhooks/subscriptions/${subscription_id}`);
|
|
5816
|
+
return { content: [{
|
|
5817
|
+
type: "text",
|
|
5818
|
+
text: JSON.stringify(result, null, 2)
|
|
5819
|
+
}] };
|
|
5820
|
+
});
|
|
5821
|
+
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.", {
|
|
5822
|
+
project_id: z.string().describe("The project ID"),
|
|
5823
|
+
subscription_id: z.string().describe("The subscription UUID"),
|
|
5824
|
+
event_name: z.string().optional().describe("New event name. Must match ^[a-z][a-z0-9_.-]{0,127}$ (case-insensitive)."),
|
|
5825
|
+
target_url: z.string().optional().describe("New HTTPS endpoint. http:// allowed only for localhost."),
|
|
5826
|
+
status: z.enum(["active", "disabled"]).optional().describe("Enable or disable delivery. \"degraded\" cannot be set manually.")
|
|
5827
|
+
}, async ({ project_id, subscription_id, event_name, target_url, status }, { client }) => {
|
|
5828
|
+
const body = {};
|
|
5829
|
+
if (event_name !== void 0) body.event_name = event_name;
|
|
5830
|
+
if (target_url !== void 0) body.target_url = target_url;
|
|
5831
|
+
if (status !== void 0) body.status = status;
|
|
5832
|
+
const result = await client.patch(`/projects/${project_id}/webhooks/subscriptions/${subscription_id}`, body);
|
|
5833
|
+
return { content: [{
|
|
5834
|
+
type: "text",
|
|
5835
|
+
text: JSON.stringify(result, null, 2)
|
|
5836
|
+
}] };
|
|
5837
|
+
});
|
|
5838
|
+
registerTool(server, apiClient, "amba_webhooks_delete", "Delete a webhook subscription. No further events are delivered to its target_url after this.", {
|
|
5839
|
+
project_id: z.string().describe("The project ID"),
|
|
5840
|
+
subscription_id: z.string().describe("The subscription UUID")
|
|
5841
|
+
}, async ({ project_id, subscription_id }, { client }) => {
|
|
5842
|
+
const result = await client.delete(`/projects/${project_id}/webhooks/subscriptions/${subscription_id}`);
|
|
5843
|
+
return { content: [{
|
|
5844
|
+
type: "text",
|
|
5845
|
+
text: JSON.stringify(result, null, 2)
|
|
5846
|
+
}] };
|
|
5847
|
+
});
|
|
5848
|
+
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.", {
|
|
5849
|
+
project_id: z.string().describe("The project ID"),
|
|
5850
|
+
subscription_id: z.string().describe("The subscription UUID")
|
|
5851
|
+
}, async ({ project_id, subscription_id }, { client }) => {
|
|
5852
|
+
const result = await client.post(`/projects/${project_id}/webhooks/subscriptions/${subscription_id}/rotate-secret`);
|
|
5853
|
+
return { content: [{
|
|
5854
|
+
type: "text",
|
|
5855
|
+
text: JSON.stringify(result, null, 2)
|
|
5856
|
+
}] };
|
|
5857
|
+
});
|
|
5858
|
+
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.", {
|
|
5859
|
+
project_id: z.string().describe("The project ID"),
|
|
5860
|
+
subscription_id: z.string().describe("The subscription UUID")
|
|
5861
|
+
}, async ({ project_id, subscription_id }, { client }) => {
|
|
5862
|
+
const result = await client.post(`/projects/${project_id}/webhooks/subscriptions/${subscription_id}/test`);
|
|
5863
|
+
return { content: [{
|
|
5864
|
+
type: "text",
|
|
5865
|
+
text: JSON.stringify(result, null, 2)
|
|
5866
|
+
}] };
|
|
5867
|
+
});
|
|
5868
|
+
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.", {
|
|
5869
|
+
project_id: z.string().describe("The project ID"),
|
|
5870
|
+
subscription_id: z.string().optional().describe("Filter to deliveries for a single subscription UUID."),
|
|
5871
|
+
status: z.enum([
|
|
5872
|
+
"pending",
|
|
5873
|
+
"delivered",
|
|
5874
|
+
"failed",
|
|
5875
|
+
"dlq"
|
|
5876
|
+
]).optional().describe("Filter by delivery status."),
|
|
5877
|
+
limit: z.number().optional().describe("Page size (default 50, max 200).")
|
|
5878
|
+
}, async ({ project_id, subscription_id, status, limit }, { client }) => {
|
|
5879
|
+
const query = {};
|
|
5880
|
+
if (subscription_id !== void 0) query.subscription_id = subscription_id;
|
|
5881
|
+
if (status !== void 0) query.status = status;
|
|
5882
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
5883
|
+
const result = await client.get(`/projects/${project_id}/webhooks/deliveries`, query);
|
|
5884
|
+
return { content: [{
|
|
5885
|
+
type: "text",
|
|
5886
|
+
text: JSON.stringify(result, null, 2)
|
|
5887
|
+
}] };
|
|
5888
|
+
});
|
|
5889
|
+
registerTool(server, apiClient, "amba_webhooks_deliveries_get", "Describe a single webhook delivery by id, including the full attempt log, attempt count, and last error.", {
|
|
5890
|
+
project_id: z.string().describe("The project ID"),
|
|
5891
|
+
delivery_id: z.string().describe("The delivery UUID")
|
|
5892
|
+
}, async ({ project_id, delivery_id }, { client }) => {
|
|
5893
|
+
const result = await client.get(`/projects/${project_id}/webhooks/deliveries/${delivery_id}`);
|
|
5894
|
+
return { content: [{
|
|
5895
|
+
type: "text",
|
|
5896
|
+
text: JSON.stringify(result, null, 2)
|
|
5897
|
+
}] };
|
|
5898
|
+
});
|
|
5899
|
+
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.", {
|
|
5900
|
+
project_id: z.string().describe("The project ID"),
|
|
5901
|
+
delivery_id: z.string().describe("The delivery UUID to replay")
|
|
5902
|
+
}, async ({ project_id, delivery_id }, { client }) => {
|
|
5903
|
+
const result = await client.post(`/projects/${project_id}/webhooks/deliveries/${delivery_id}/replay`);
|
|
5904
|
+
return { content: [{
|
|
5905
|
+
type: "text",
|
|
5906
|
+
text: JSON.stringify(result, null, 2)
|
|
5907
|
+
}] };
|
|
5908
|
+
});
|
|
5909
|
+
}
|
|
5910
|
+
//#endregion
|
|
5911
|
+
//#region src/tools/email.ts
|
|
5912
|
+
function registerTools(server, apiClient) {
|
|
5913
|
+
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_]*$.", {
|
|
5914
|
+
project_id: z.string().describe("The project ID"),
|
|
5915
|
+
name: z.string().describe("Template name. Must match ^[a-z][a-z0-9_]*$."),
|
|
5916
|
+
subject: z.string().describe("Email subject line. Supports template variables."),
|
|
5917
|
+
html_body: z.string().describe("HTML body. Supports template variables."),
|
|
5918
|
+
text_body: z.string().nullable().optional().describe("Optional plain-text fallback body. Pass null to leave unset.")
|
|
5919
|
+
}, async ({ project_id, name, subject, html_body, text_body }, { client }) => {
|
|
5920
|
+
const body = {
|
|
5921
|
+
name,
|
|
5922
|
+
subject,
|
|
5923
|
+
html_body
|
|
5924
|
+
};
|
|
5925
|
+
if (text_body !== void 0) body.text_body = text_body;
|
|
5926
|
+
const result = await client.post(`/projects/${project_id}/email/templates`, body);
|
|
5927
|
+
return { content: [{
|
|
5928
|
+
type: "text",
|
|
5929
|
+
text: JSON.stringify(result, null, 2)
|
|
5930
|
+
}] };
|
|
5931
|
+
});
|
|
5932
|
+
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 }) => {
|
|
5933
|
+
const result = await client.get(`/projects/${project_id}/email/templates`);
|
|
5934
|
+
return { content: [{
|
|
5935
|
+
type: "text",
|
|
5936
|
+
text: JSON.stringify(result, null, 2)
|
|
5937
|
+
}] };
|
|
5938
|
+
});
|
|
5939
|
+
registerTool(server, apiClient, "amba_email_templates_get", "Get a single transactional email template by name, including its subject, html_body, and text_body.", {
|
|
5940
|
+
project_id: z.string().describe("The project ID"),
|
|
5941
|
+
name: z.string().describe("The template name")
|
|
5942
|
+
}, async ({ project_id, name }, { client }) => {
|
|
5943
|
+
const result = await client.get(`/projects/${project_id}/email/templates/${encodeURIComponent(name)}`);
|
|
5944
|
+
return { content: [{
|
|
5945
|
+
type: "text",
|
|
5946
|
+
text: JSON.stringify(result, null, 2)
|
|
5947
|
+
}] };
|
|
5948
|
+
});
|
|
5949
|
+
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.", {
|
|
5950
|
+
project_id: z.string().describe("The project ID"),
|
|
5951
|
+
name: z.string().describe("The template name to update"),
|
|
5952
|
+
subject: z.string().optional().describe("New subject line."),
|
|
5953
|
+
html_body: z.string().optional().describe("New HTML body."),
|
|
5954
|
+
text_body: z.string().nullable().optional().describe("New plain-text body; pass null to clear it, omit to leave unchanged.")
|
|
5955
|
+
}, async ({ project_id, name, subject, html_body, text_body }, { client }) => {
|
|
5956
|
+
const body = {};
|
|
5957
|
+
if (subject !== void 0) body.subject = subject;
|
|
5958
|
+
if (html_body !== void 0) body.html_body = html_body;
|
|
5959
|
+
if (text_body !== void 0) body.text_body = text_body;
|
|
5960
|
+
const result = await client.patch(`/projects/${project_id}/email/templates/${encodeURIComponent(name)}`, body);
|
|
5961
|
+
return { content: [{
|
|
5962
|
+
type: "text",
|
|
5963
|
+
text: JSON.stringify(result, null, 2)
|
|
5964
|
+
}] };
|
|
5965
|
+
});
|
|
5966
|
+
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.", {
|
|
5967
|
+
project_id: z.string().describe("The project ID"),
|
|
5968
|
+
name: z.string().describe("The template name to delete")
|
|
5969
|
+
}, async ({ project_id, name }, { client }) => {
|
|
5970
|
+
const result = await client.delete(`/projects/${project_id}/email/templates/${encodeURIComponent(name)}`);
|
|
5971
|
+
return { content: [{
|
|
5972
|
+
type: "text",
|
|
5973
|
+
text: JSON.stringify(result, null, 2)
|
|
5974
|
+
}] };
|
|
5975
|
+
});
|
|
5976
|
+
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.", {
|
|
5977
|
+
project_id: z.string().describe("The project ID"),
|
|
5978
|
+
email: z.string().describe("Email address to suppress (must contain @)."),
|
|
5979
|
+
reason: z.enum([
|
|
5980
|
+
"bounce",
|
|
5981
|
+
"complaint",
|
|
5982
|
+
"manual"
|
|
5983
|
+
]).optional().describe("Why the address is suppressed. Defaults to \"manual\".")
|
|
5984
|
+
}, async ({ project_id, email, reason }, { client }) => {
|
|
5985
|
+
const body = { email };
|
|
5986
|
+
if (reason !== void 0) body.reason = reason;
|
|
5987
|
+
const result = await client.post(`/projects/${project_id}/email/suppressions`, body);
|
|
5988
|
+
return { content: [{
|
|
5989
|
+
type: "text",
|
|
5990
|
+
text: JSON.stringify(result, null, 2)
|
|
5991
|
+
}] };
|
|
5992
|
+
});
|
|
5993
|
+
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\").", {
|
|
5994
|
+
project_id: z.string().describe("The project ID"),
|
|
5995
|
+
reason: z.enum([
|
|
5996
|
+
"bounce",
|
|
5997
|
+
"complaint",
|
|
5998
|
+
"manual"
|
|
5999
|
+
]).optional().describe("Filter to a single suppression reason.")
|
|
6000
|
+
}, async ({ project_id, reason }, { client }) => {
|
|
6001
|
+
const query = {};
|
|
6002
|
+
if (reason !== void 0) query.reason = reason;
|
|
6003
|
+
const result = await client.get(`/projects/${project_id}/email/suppressions`, query);
|
|
6004
|
+
return { content: [{
|
|
6005
|
+
type: "text",
|
|
6006
|
+
text: JSON.stringify(result, null, 2)
|
|
6007
|
+
}] };
|
|
6008
|
+
});
|
|
6009
|
+
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.", {
|
|
6010
|
+
project_id: z.string().describe("The project ID"),
|
|
6011
|
+
email: z.string().describe("Email address to unsuppress.")
|
|
6012
|
+
}, async ({ project_id, email }, { client }) => {
|
|
6013
|
+
const result = await client.delete(`/projects/${project_id}/email/suppressions/${encodeURIComponent(email)}`);
|
|
6014
|
+
return { content: [{
|
|
6015
|
+
type: "text",
|
|
6016
|
+
text: JSON.stringify(result, null, 2)
|
|
6017
|
+
}] };
|
|
6018
|
+
});
|
|
6019
|
+
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\".", {
|
|
6020
|
+
project_id: z.string().describe("The project ID"),
|
|
6021
|
+
to: z.object({
|
|
6022
|
+
user_id: z.string().optional().describe("App user UUID; resolves the user's email server-side."),
|
|
6023
|
+
email: z.string().optional().describe("Raw recipient email address.")
|
|
6024
|
+
}).describe("Recipient. Provide exactly one of user_id or email."),
|
|
6025
|
+
template: z.string().optional().describe("Template name to render. Mutually exclusive with subject/html/text."),
|
|
6026
|
+
data: z.record(z.unknown()).optional().describe("Template variable map (used with `template`)."),
|
|
6027
|
+
subject: z.string().optional().describe("Ad-hoc subject. Requires to.email; mutually exclusive with `template`."),
|
|
6028
|
+
html: z.string().optional().describe("Ad-hoc HTML body (ad-hoc send only)."),
|
|
6029
|
+
text: z.string().optional().describe("Ad-hoc plain-text body (ad-hoc send only).")
|
|
6030
|
+
}, async ({ project_id, to, template, data, subject, html, text }, { client }) => {
|
|
6031
|
+
const body = { to };
|
|
6032
|
+
if (template !== void 0) body.template = template;
|
|
6033
|
+
if (data !== void 0) body.data = data;
|
|
6034
|
+
if (subject !== void 0) body.subject = subject;
|
|
6035
|
+
if (html !== void 0) body.html = html;
|
|
6036
|
+
if (text !== void 0) body.text = text;
|
|
6037
|
+
const result = await client.post(`/projects/${project_id}/email/send`, body);
|
|
6038
|
+
return { content: [{
|
|
6039
|
+
type: "text",
|
|
6040
|
+
text: JSON.stringify(result, null, 2)
|
|
6041
|
+
}] };
|
|
6042
|
+
});
|
|
6043
|
+
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.", {
|
|
6044
|
+
project_id: z.string().describe("The project ID"),
|
|
6045
|
+
template: z.string().optional().describe("Filter to a single template name."),
|
|
6046
|
+
status: z.enum([
|
|
6047
|
+
"queued",
|
|
6048
|
+
"sent",
|
|
6049
|
+
"suppressed",
|
|
6050
|
+
"failed"
|
|
6051
|
+
]).optional().describe("Filter by delivery status."),
|
|
6052
|
+
limit: z.number().optional().describe("Page size (default 50, max 200).")
|
|
6053
|
+
}, async ({ project_id, template, status, limit }, { client }) => {
|
|
6054
|
+
const query = {};
|
|
6055
|
+
if (template !== void 0) query.template = template;
|
|
6056
|
+
if (status !== void 0) query.status = status;
|
|
6057
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
6058
|
+
const result = await client.get(`/projects/${project_id}/email/deliveries`, query);
|
|
6059
|
+
return { content: [{
|
|
6060
|
+
type: "text",
|
|
6061
|
+
text: JSON.stringify(result, null, 2)
|
|
6062
|
+
}] };
|
|
6063
|
+
});
|
|
6064
|
+
registerTool(server, apiClient, "amba_email_deliveries_get", "Describe a single email delivery by id, including recipient, template, status, and last error if it failed.", {
|
|
6065
|
+
project_id: z.string().describe("The project ID"),
|
|
6066
|
+
delivery_id: z.string().describe("The delivery UUID")
|
|
6067
|
+
}, async ({ project_id, delivery_id }, { client }) => {
|
|
6068
|
+
const result = await client.get(`/projects/${project_id}/email/deliveries/${delivery_id}`);
|
|
6069
|
+
return { content: [{
|
|
6070
|
+
type: "text",
|
|
6071
|
+
text: JSON.stringify(result, null, 2)
|
|
6072
|
+
}] };
|
|
6073
|
+
});
|
|
4790
6074
|
}
|
|
4791
6075
|
//#endregion
|
|
4792
6076
|
//#region src/resources/amba-setup-identity.ts
|
|
@@ -5586,6 +6870,7 @@ Real-money purchases need a billing integration configured separately — see \`
|
|
|
5586
6870
|
| \`amba_currencies_update\` | Edit (rename, change caps, change auto-recharge). | \`{ project_id, currency_id, max_balance: 10000 }\` |
|
|
5587
6871
|
| \`amba_currencies_delete\` | Delete a currency (irreversible — users lose their balance). | \`{ project_id, currency_id }\` |
|
|
5588
6872
|
| \`amba_currencies_grant\` | Grant currency to a specific user. | \`{ project_id, app_user_id, currency_code: "gems", amount: 100, reason: "welcome_bonus" }\` |
|
|
6873
|
+
| \`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
6874
|
| \`amba_currencies_get_transactions\` | Per-user transaction ledger. | \`{ project_id, user_id, currency_code: "gems", limit: 100 }\` |
|
|
5590
6875
|
|
|
5591
6876
|
#### Currency grant rules (auto-grants)
|
|
@@ -5871,6 +7156,9 @@ Mostly SDK-side — admin tools are for moderation + stats.
|
|
|
5871
7156
|
|
|
5872
7157
|
| Tool | Purpose | Example args |
|
|
5873
7158
|
| --- | --- | --- |
|
|
7159
|
+
| \`amba_messaging_create_conversation\` | Create a conversation server-side (zero or more initial participants — for matchmaker-built circles). | \`{ project_id, type: "group", name: "Circle" }\` |
|
|
7160
|
+
| \`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 }\` |
|
|
7161
|
+
| \`amba_messaging_remove_participant\` | Remove a user from a conversation (idempotent). | \`{ project_id, conversation_id, user_id }\` |
|
|
5874
7162
|
| \`amba_messaging_list_conversations\` | List conversations (admin / moderation view). | \`{ project_id, limit: 50 }\` |
|
|
5875
7163
|
| \`amba_messaging_list_messages\` | List messages in a conversation. | \`{ project_id, conversation_id, limit: 100 }\` |
|
|
5876
7164
|
| \`amba_messaging_delete_message\` | Hard-delete a message (moderation). | \`{ project_id, conversation_id, message_id }\` |
|
|
@@ -6092,25 +7380,28 @@ If gamification, economy, and social are the playable surface, **infrastructure
|
|
|
6092
7380
|
|
|
6093
7381
|
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
7382
|
|
|
7383
|
+
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\`.
|
|
7384
|
+
|
|
6095
7385
|
| Tool | Purpose | Example args |
|
|
6096
7386
|
| --- | --- | --- |
|
|
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 }] }\` |
|
|
7387
|
+
| \`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
7388
|
| \`amba_collections_list\` | List collections in this project. | \`{ project_id }\` |
|
|
6099
|
-
| \`amba_collections_get\` | Read one collection's schema. | \`{ project_id,
|
|
6100
|
-
| \`amba_collections_alter\` |
|
|
6101
|
-
| \`amba_collections_delete\` | Drop the table (destructive). | \`{ project_id,
|
|
6102
|
-
| \`amba_admin_insert_row\` | Insert
|
|
6103
|
-
| \`
|
|
6104
|
-
| \`
|
|
6105
|
-
| \`
|
|
6106
|
-
| \`
|
|
6107
|
-
| \`
|
|
6108
|
-
| \`
|
|
6109
|
-
| \`
|
|
6110
|
-
| \`
|
|
6111
|
-
| \`
|
|
6112
|
-
|
|
6113
|
-
|
|
7389
|
+
| \`amba_collections_get\` | Read one collection's schema. | \`{ project_id, name: "todos" }\` |
|
|
7390
|
+
| \`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: "integer", nullable: true } }\` |
|
|
7391
|
+
| \`amba_collections_delete\` | Drop the table (destructive). \`confirm\` must equal the collection name. | \`{ project_id, name: "todos", confirm: "todos" }\` |
|
|
7392
|
+
| \`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 } }\` |
|
|
7393
|
+
| \`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" }\` |
|
|
7394
|
+
| \`amba_admin_list_rows\` | Read rows as the developer. | \`{ project_id, name: "todos", limit: 100 }\` |
|
|
7395
|
+
| \`amba_client_insert_row\` | Insert as an end-user. Requires \`api_key\` (+ \`session_token\`). | \`{ api_key, session_token, name: "todos", row: {...} }\` |
|
|
7396
|
+
| \`amba_client_list_rows\` | Read as an end-user (auto user-scoped). | \`{ api_key, session_token, name: "todos" }\` |
|
|
7397
|
+
| \`amba_client_get_row\` | Get one row by id (end-user). | \`{ api_key, session_token, name: "todos", id }\` |
|
|
7398
|
+
| \`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: {...} }\` |
|
|
7399
|
+
| \`amba_client_delete_row\` | Soft-delete one row by id (end-user). | \`{ api_key, session_token, name: "todos", id }\` |
|
|
7400
|
+
| \`amba_client_count_rows\` | Count rows matching an optional \`where\`. | \`{ api_key, session_token, name: "todos", where: {...} }\` |
|
|
7401
|
+
| \`amba_client_find_rows\` | Filter / sort / paginate rows (SDK-shaped \`filter\`). | \`{ api_key, session_token, name: "todos", filter: {...}, order: ["created_at desc"], limit: 50 }\` |
|
|
7402
|
+
| \`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 }\` |
|
|
7403
|
+
|
|
7404
|
+
Column types: \`text\`, \`integer\`, \`bigint\`, \`numeric\`, \`boolean\`, \`timestamptz\`, \`date\`, \`jsonb\`, \`uuid\`, \`vector(<dim>)\` (e.g. \`vector(1536)\` for OpenAI embeddings), plus array forms \`text[]\`, \`integer[]\`, \`bigint[]\`, \`numeric[]\`, \`boolean[]\`, \`uuid[]\`. Use \`integer\` (not \`int\`), \`numeric\` (not \`float\`/\`real\`/\`double\`), and \`jsonb\` (not \`json\`) — the validator rejects the aliases.
|
|
6114
7405
|
|
|
6115
7406
|
### Functions (serverless code)
|
|
6116
7407
|
|
|
@@ -6130,16 +7421,23 @@ Run user code in a sandbox triggered by HTTP, cron, or webhook. The function get
|
|
|
6130
7421
|
|
|
6131
7422
|
### AI prompts
|
|
6132
7423
|
|
|
6133
|
-
Managed LLM templates: stored prompt with model + system message
|
|
7424
|
+
Managed LLM templates: a stored prompt with provider + model + system message, invoked by name from the SDK. The actual LLM call is rewritten server-side per-tenant — the customer's provider API key (Anthropic / OpenAI / Mistral / Gemini) stays server-side, never on the device.
|
|
7425
|
+
|
|
7426
|
+
**Two steps, in order:** first register the provider key with \`amba_ai_providers_set\`, then create prompts against it. A prompt registered before its provider has a key still saves, but invocations fail with \`provider_not_configured\` (424) until the key is set.
|
|
7427
|
+
|
|
7428
|
+
> The provider key is **not** a function secret. \`amba_secrets_set\` writes function-scoped Worker secrets, which the AI gateway never reads. Provider keys live in a separate gateway-owned store and are set **only** via \`amba_ai_providers_set\`.
|
|
6134
7429
|
|
|
6135
7430
|
| Tool | Purpose | Example args |
|
|
6136
7431
|
| --- | --- | --- |
|
|
6137
|
-
| \`
|
|
7432
|
+
| \`amba_ai_providers_set\` | Register / rotate the upstream provider API key. **Do this first.** | \`{ project_id, provider: "anthropic", api_key: "sk-ant-..." }\` |
|
|
7433
|
+
| \`amba_ai_providers_list\` | List registered providers (\`configured\` = key set). | \`{ project_id }\` |
|
|
7434
|
+
| \`amba_ai_providers_delete\` | Revoke a provider key (fails if prompts still reference it). | \`{ project_id, provider: "anthropic" }\` |
|
|
7435
|
+
| \`amba_ai_prompts_create\` | Create a prompt template. \`client_invokable: true\` lets the device SDK invoke it directly. | \`{ project_id, name: "summarize", provider: "anthropic", model: "claude-opus-4-5", system_prompt: "Summarize the user's text in 2 sentences.", client_invokable: true }\` |
|
|
6138
7436
|
| \`amba_ai_prompts_list\` | List prompts. | \`{ project_id }\` |
|
|
6139
|
-
| \`amba_ai_prompts_get\` | Read one prompt. | \`{ project_id,
|
|
6140
|
-
| \`amba_ai_prompts_update\` | Edit a prompt. | \`{ project_id,
|
|
6141
|
-
| \`amba_ai_prompts_invoke\` | Invoke a prompt server-side (admin testing). | \`{ project_id,
|
|
6142
|
-
| \`amba_ai_prompts_delete\` | Delete. | \`{ project_id,
|
|
7437
|
+
| \`amba_ai_prompts_get\` | Read one prompt. | \`{ project_id, name }\` |
|
|
7438
|
+
| \`amba_ai_prompts_update\` | Edit a prompt (replaces all fields; bumps version). | \`{ project_id, name, provider, model, system_prompt: "..." }\` |
|
|
7439
|
+
| \`amba_ai_prompts_invoke\` | Invoke a prompt server-side (admin testing). \`messages\` is a provider-shaped array. | \`{ project_id, name, messages: [{ role: "user", content: "..." }] }\` |
|
|
7440
|
+
| \`amba_ai_prompts_delete\` | Delete. | \`{ project_id, name }\` |
|
|
6143
7441
|
|
|
6144
7442
|
### Analytics + events + sessions
|
|
6145
7443
|
|
|
@@ -6155,9 +7453,11 @@ Managed LLM templates: stored prompt with model + system message + variables, ca
|
|
|
6155
7453
|
|
|
6156
7454
|
### Secrets + configs + integrations
|
|
6157
7455
|
|
|
7456
|
+
Secrets here are **function-scoped** — they become environment bindings on your deployed functions. They are NOT where AI provider keys go (use \`amba_ai_providers_set\` for those — see AI prompts above).
|
|
7457
|
+
|
|
6158
7458
|
| Tool | Purpose | Example args |
|
|
6159
7459
|
| --- | --- | --- |
|
|
6160
|
-
| \`amba_secrets_set\` | Set a
|
|
7460
|
+
| \`amba_secrets_set\` | Set a function-scoped secret (encrypted at rest; bound on the next deploy). | \`{ project_id, name: "STRIPE_WEBHOOK_SECRET", value: "whsec_..." }\` |
|
|
6161
7461
|
| \`amba_secrets_get\` | Read a secret (returns \`"<redacted>"\` unless explicitly requested). | \`{ project_id, name }\` |
|
|
6162
7462
|
| \`amba_secrets_list\` | List secret names. | \`{ project_id }\` |
|
|
6163
7463
|
| \`amba_secrets_delete\` | Delete. | \`{ project_id, name }\` |
|
|
@@ -6216,9 +7516,9 @@ const newTodo = await Amba.collections.insert('todos', { title: 'Ship the app',
|
|
|
6216
7516
|
await Amba.collections.update('todos', newTodo.id, { done: true });
|
|
6217
7517
|
await Amba.collections.delete('todos', newTodo.id);
|
|
6218
7518
|
|
|
6219
|
-
// AI — call a managed prompt
|
|
7519
|
+
// AI — call a managed prompt (prompt_slug names the registered prompt)
|
|
6220
7520
|
const response = await Amba.ai.anthropic.messages.create({
|
|
6221
|
-
|
|
7521
|
+
prompt_slug: 'summarize',
|
|
6222
7522
|
variables: { text: 'A long article about backend services …' },
|
|
6223
7523
|
});
|
|
6224
7524
|
|
|
@@ -6286,8 +7586,7 @@ let showBeta = try await Amba.flags.get(name: "beta_feature")
|
|
|
6286
7586
|
try await Amba.events.track("app_opened", properties: ["source": "deep_link"])
|
|
6287
7587
|
|
|
6288
7588
|
let reply = try await Amba.ai.anthropic.messages.create(
|
|
6289
|
-
|
|
6290
|
-
variables: ["text": "A long article..."]
|
|
7589
|
+
request: AiMessageRequest(promptSlug: "summarize", variables: ["text": "A long article..."])
|
|
6291
7590
|
)
|
|
6292
7591
|
\`\`\`
|
|
6293
7592
|
|
|
@@ -6344,7 +7643,7 @@ Batch.
|
|
|
6344
7643
|
- [ ] Resend (transactional email)
|
|
6345
7644
|
- [ ] Stripe (web payments / subscriptions)
|
|
6346
7645
|
- [ ] Mixpanel / PostHog / Segment (analytics forwarding)
|
|
6347
|
-
- [ ] OpenAI / Anthropic
|
|
7646
|
+
- [ ] OpenAI / Anthropic / Mistral / Gemini LLM keys (required for \`Amba.ai.*\` — set via \`amba_ai_providers_set\`, **not** \`amba_integrations_configure\`)
|
|
6348
7647
|
|
|
6349
7648
|
6. **Feature flags:** seed any starter flags?
|
|
6350
7649
|
- Yes — wire \`beta_feature\` (off by default) so I can ship the wiring before the feature exists
|
|
@@ -6359,7 +7658,7 @@ Batch.
|
|
|
6359
7658
|
1. Before creating:
|
|
6360
7659
|
- \`amba_collections_list\` — match on \`name\`. Collisions: never silently recreate (data loss). Offer \`amba_collections_alter\` to add new columns instead.
|
|
6361
7660
|
- \`amba_functions_list\` — match on \`name\`. Collisions: ask to redeploy (with the new source) or skip.
|
|
6362
|
-
- \`amba_ai_prompts_list\` — match on \`
|
|
7661
|
+
- \`amba_ai_prompts_list\` — match on \`name\`. Same. (And \`amba_ai_providers_list\` — match on \`provider\`; re-running \`amba_ai_providers_set\` rotates the key in place.)
|
|
6363
7662
|
- \`amba_integrations_list\` — match on \`provider\`. Same.
|
|
6364
7663
|
- \`amba_configs_list\` — match on \`key\`. Same.
|
|
6365
7664
|
|
|
@@ -6541,6 +7840,8 @@ const TOOL_CATEGORY = {
|
|
|
6541
7840
|
amba_developer_rotate_pat: "identity",
|
|
6542
7841
|
amba_users_list: "identity",
|
|
6543
7842
|
amba_list_users: "identity",
|
|
7843
|
+
amba_users_create: "identity",
|
|
7844
|
+
amba_create_user: "identity",
|
|
6544
7845
|
amba_users_get: "identity",
|
|
6545
7846
|
amba_users_delete: "identity",
|
|
6546
7847
|
amba_users_export: "identity",
|
|
@@ -6589,6 +7890,7 @@ const TOOL_CATEGORY = {
|
|
|
6589
7890
|
amba_onboarding_get_stats: "engagement",
|
|
6590
7891
|
amba_get_onboarding_stats: "engagement",
|
|
6591
7892
|
amba_content_libraries_create: "engagement",
|
|
7893
|
+
amba_content_libraries_delete: "engagement",
|
|
6592
7894
|
amba_create_content_library: "engagement",
|
|
6593
7895
|
amba_content_list_libraries: "engagement",
|
|
6594
7896
|
amba_content_items_add: "engagement",
|
|
@@ -6623,6 +7925,8 @@ const TOOL_CATEGORY = {
|
|
|
6623
7925
|
amba_list_xp_rules: "gamification",
|
|
6624
7926
|
amba_xp_update_rule: "gamification",
|
|
6625
7927
|
amba_xp_delete_rule: "gamification",
|
|
7928
|
+
amba_xp_grant: "gamification",
|
|
7929
|
+
amba_xp_deduct: "gamification",
|
|
6626
7930
|
amba_xp_list_users: "gamification",
|
|
6627
7931
|
amba_users_get_xp: "gamification",
|
|
6628
7932
|
amba_get_user_xp: "gamification",
|
|
@@ -6636,11 +7940,14 @@ const TOOL_CATEGORY = {
|
|
|
6636
7940
|
amba_update_achievement: "gamification",
|
|
6637
7941
|
amba_achievements_delete: "gamification",
|
|
6638
7942
|
amba_delete_achievement: "gamification",
|
|
7943
|
+
amba_achievements_grant: "gamification",
|
|
7944
|
+
amba_achievements_revoke: "gamification",
|
|
6639
7945
|
amba_streaks_create: "gamification",
|
|
6640
7946
|
amba_create_streak: "gamification",
|
|
6641
7947
|
amba_streaks_list: "gamification",
|
|
6642
7948
|
amba_streaks_update: "gamification",
|
|
6643
7949
|
amba_streaks_delete: "gamification",
|
|
7950
|
+
amba_streaks_reset: "gamification",
|
|
6644
7951
|
amba_leaderboards_create: "gamification",
|
|
6645
7952
|
amba_create_leaderboard: "gamification",
|
|
6646
7953
|
amba_leaderboards_list: "gamification",
|
|
@@ -6653,6 +7960,14 @@ const TOOL_CATEGORY = {
|
|
|
6653
7960
|
amba_update_leaderboard: "gamification",
|
|
6654
7961
|
amba_leaderboards_delete: "gamification",
|
|
6655
7962
|
amba_delete_leaderboard: "gamification",
|
|
7963
|
+
amba_leagues_create: "gamification",
|
|
7964
|
+
amba_create_league: "gamification",
|
|
7965
|
+
amba_leagues_list: "gamification",
|
|
7966
|
+
amba_list_leagues: "gamification",
|
|
7967
|
+
amba_leagues_update: "gamification",
|
|
7968
|
+
amba_update_league: "gamification",
|
|
7969
|
+
amba_leagues_list_cohorts: "gamification",
|
|
7970
|
+
amba_list_league_cohorts: "gamification",
|
|
6656
7971
|
amba_challenges_create: "gamification",
|
|
6657
7972
|
amba_create_challenge: "gamification",
|
|
6658
7973
|
amba_challenges_list: "gamification",
|
|
@@ -6675,6 +7990,7 @@ const TOOL_CATEGORY = {
|
|
|
6675
7990
|
amba_delete_currency: "economy",
|
|
6676
7991
|
amba_currencies_grant: "economy",
|
|
6677
7992
|
amba_grant_currency: "economy",
|
|
7993
|
+
amba_currencies_spend: "economy",
|
|
6678
7994
|
amba_currencies_get_transactions: "economy",
|
|
6679
7995
|
amba_get_currency_transactions: "economy",
|
|
6680
7996
|
amba_currency_grant_rules_create: "economy",
|
|
@@ -6709,6 +8025,7 @@ const TOOL_CATEGORY = {
|
|
|
6709
8025
|
amba_stores_patch_listing: "economy",
|
|
6710
8026
|
amba_stores_delete_listing: "economy",
|
|
6711
8027
|
amba_inventory_grant_item: "economy",
|
|
8028
|
+
amba_inventory_revoke_item: "economy",
|
|
6712
8029
|
amba_grant_item: "economy",
|
|
6713
8030
|
amba_users_get_inventory: "economy",
|
|
6714
8031
|
amba_get_user_inventory: "economy",
|
|
@@ -6732,6 +8049,9 @@ const TOOL_CATEGORY = {
|
|
|
6732
8049
|
amba_update_group_member: "social",
|
|
6733
8050
|
amba_groups_remove_member: "social",
|
|
6734
8051
|
amba_remove_group_member: "social",
|
|
8052
|
+
amba_messaging_create_conversation: "social",
|
|
8053
|
+
amba_messaging_add_participant: "social",
|
|
8054
|
+
amba_messaging_remove_participant: "social",
|
|
6735
8055
|
amba_messaging_list_conversations: "social",
|
|
6736
8056
|
amba_messaging_list_messages: "social",
|
|
6737
8057
|
amba_messaging_delete_message: "social",
|
|
@@ -6770,6 +8090,20 @@ const TOOL_CATEGORY = {
|
|
|
6770
8090
|
amba_get_analytics: "analytics",
|
|
6771
8091
|
amba_sessions_list: "analytics",
|
|
6772
8092
|
amba_sessions_analytics: "analytics",
|
|
8093
|
+
amba_funnels_list: "analytics",
|
|
8094
|
+
amba_funnels_create: "analytics",
|
|
8095
|
+
amba_funnels_get: "analytics",
|
|
8096
|
+
amba_funnels_update: "analytics",
|
|
8097
|
+
amba_funnels_delete: "analytics",
|
|
8098
|
+
amba_funnels_query: "analytics",
|
|
8099
|
+
amba_experiments_create: "analytics",
|
|
8100
|
+
amba_experiments_list: "analytics",
|
|
8101
|
+
amba_experiments_get: "analytics",
|
|
8102
|
+
amba_experiments_update: "analytics",
|
|
8103
|
+
amba_experiments_delete: "analytics",
|
|
8104
|
+
amba_experiments_results: "analytics",
|
|
8105
|
+
amba_project_export: "infrastructure",
|
|
8106
|
+
amba_project_import: "infrastructure",
|
|
6773
8107
|
amba_projects_create: "infrastructure",
|
|
6774
8108
|
amba_create_project: "infrastructure",
|
|
6775
8109
|
amba_projects_list: "infrastructure",
|
|
@@ -6796,6 +8130,7 @@ const TOOL_CATEGORY = {
|
|
|
6796
8130
|
amba_delete_collection: "infrastructure",
|
|
6797
8131
|
amba_admin_insert_row: "infrastructure",
|
|
6798
8132
|
amba_admin_list_rows: "infrastructure",
|
|
8133
|
+
amba_admin_aggregate_rows: "infrastructure",
|
|
6799
8134
|
amba_client_insert_row: "infrastructure",
|
|
6800
8135
|
amba_client_list_rows: "infrastructure",
|
|
6801
8136
|
amba_client_get_row: "infrastructure",
|
|
@@ -6836,6 +8171,10 @@ const TOOL_CATEGORY = {
|
|
|
6836
8171
|
amba_remove_site_domain: "infrastructure",
|
|
6837
8172
|
amba_sites_list_domains: "infrastructure",
|
|
6838
8173
|
amba_list_site_domains: "infrastructure",
|
|
8174
|
+
amba_domains_search: "infrastructure",
|
|
8175
|
+
amba_domains_check: "infrastructure",
|
|
8176
|
+
amba_domains_purchase: "infrastructure",
|
|
8177
|
+
amba_domains_list: "infrastructure",
|
|
6839
8178
|
amba_media_upload: "infrastructure",
|
|
6840
8179
|
amba_upload_media: "infrastructure",
|
|
6841
8180
|
amba_media_list: "infrastructure",
|
|
@@ -6867,6 +8206,9 @@ const TOOL_CATEGORY = {
|
|
|
6867
8206
|
amba_integrations_patch: "infrastructure",
|
|
6868
8207
|
amba_integrations_test: "infrastructure",
|
|
6869
8208
|
amba_test_integration: "infrastructure",
|
|
8209
|
+
amba_ai_providers_set: "infrastructure",
|
|
8210
|
+
amba_ai_providers_list: "infrastructure",
|
|
8211
|
+
amba_ai_providers_delete: "infrastructure",
|
|
6870
8212
|
amba_ai_prompts_create: "infrastructure",
|
|
6871
8213
|
amba_create_ai_prompt: "infrastructure",
|
|
6872
8214
|
amba_ai_prompts_list: "infrastructure",
|
|
@@ -6886,7 +8228,34 @@ const TOOL_CATEGORY = {
|
|
|
6886
8228
|
amba_billing_set_ceiling: "infrastructure",
|
|
6887
8229
|
amba_projects_invite_member: "infrastructure",
|
|
6888
8230
|
amba_projects_list_members: "infrastructure",
|
|
6889
|
-
amba_projects_remove_member: "infrastructure"
|
|
8231
|
+
amba_projects_remove_member: "infrastructure",
|
|
8232
|
+
amba_projects_transfer_owner: "infrastructure",
|
|
8233
|
+
amba_webhooks_create: "infrastructure",
|
|
8234
|
+
amba_webhooks_list: "infrastructure",
|
|
8235
|
+
amba_webhooks_get: "infrastructure",
|
|
8236
|
+
amba_webhooks_update: "infrastructure",
|
|
8237
|
+
amba_webhooks_delete: "infrastructure",
|
|
8238
|
+
amba_webhooks_rotate_secret: "infrastructure",
|
|
8239
|
+
amba_webhooks_test: "infrastructure",
|
|
8240
|
+
amba_webhooks_deliveries_list: "infrastructure",
|
|
8241
|
+
amba_webhooks_deliveries_get: "infrastructure",
|
|
8242
|
+
amba_webhooks_deliveries_replay: "infrastructure",
|
|
8243
|
+
amba_email_templates_create: "engagement",
|
|
8244
|
+
amba_email_templates_list: "engagement",
|
|
8245
|
+
amba_email_templates_get: "engagement",
|
|
8246
|
+
amba_email_templates_update: "engagement",
|
|
8247
|
+
amba_email_templates_delete: "engagement",
|
|
8248
|
+
amba_email_suppressions_create: "engagement",
|
|
8249
|
+
amba_email_suppressions_list: "engagement",
|
|
8250
|
+
amba_email_suppressions_delete: "engagement",
|
|
8251
|
+
amba_email_send: "engagement",
|
|
8252
|
+
amba_email_deliveries_list: "engagement",
|
|
8253
|
+
amba_email_deliveries_get: "engagement",
|
|
8254
|
+
amba_admin_insert_rows: "infrastructure",
|
|
8255
|
+
amba_remote_configs_create: "infrastructure",
|
|
8256
|
+
amba_remote_configs_list: "infrastructure",
|
|
8257
|
+
amba_remote_configs_update: "infrastructure",
|
|
8258
|
+
amba_remote_configs_delete: "infrastructure"
|
|
6890
8259
|
};
|
|
6891
8260
|
/**
|
|
6892
8261
|
* Look up the canonical category for an MCP tool name. Accepts both
|
|
@@ -6906,7 +8275,15 @@ function getToolCategory(toolName) {
|
|
|
6906
8275
|
* called against it once.
|
|
6907
8276
|
*/
|
|
6908
8277
|
function registerAllTools(server, apiClient) {
|
|
6909
|
-
registerTools$
|
|
8278
|
+
registerTools$13(server, apiClient);
|
|
8279
|
+
registerTools$33(server, apiClient);
|
|
8280
|
+
registerTools$32(server, apiClient);
|
|
8281
|
+
registerTools$31(server, apiClient);
|
|
8282
|
+
registerTools$30(server, apiClient);
|
|
8283
|
+
registerTools$29(server, apiClient);
|
|
8284
|
+
registerTools$28(server, apiClient);
|
|
8285
|
+
registerTools$27(server, apiClient);
|
|
8286
|
+
registerTools$26(server, apiClient);
|
|
6910
8287
|
registerTools$25(server, apiClient);
|
|
6911
8288
|
registerTools$24(server, apiClient);
|
|
6912
8289
|
registerTools$23(server, apiClient);
|
|
@@ -6919,12 +8296,12 @@ function registerAllTools(server, apiClient) {
|
|
|
6919
8296
|
registerTools$16(server, apiClient);
|
|
6920
8297
|
registerTools$15(server, apiClient);
|
|
6921
8298
|
registerTools$14(server, apiClient);
|
|
6922
|
-
registerTools$13(server, apiClient);
|
|
6923
8299
|
registerTools$12(server, apiClient);
|
|
6924
8300
|
registerTools$11(server, apiClient);
|
|
6925
8301
|
registerTools$10(server, apiClient);
|
|
6926
8302
|
registerTools$9(server, apiClient);
|
|
6927
8303
|
registerTools$8(server, apiClient);
|
|
8304
|
+
registerTools$7(server, apiClient);
|
|
6928
8305
|
registerTools$6(server, apiClient);
|
|
6929
8306
|
registerTools$5(server, apiClient);
|
|
6930
8307
|
registerTools$4(server, apiClient);
|