@layers/amba-mcp 4.0.3 → 4.0.5
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 +9 -1
- package/dist/index.js +969 -142
- package/dist/lib/annotations.d.ts +0 -7
- package/dist/resources/amba-setup-infrastructure.d.ts +1 -1
- package/dist/resources/amba-setup.d.ts +1 -1
- package/dist/resources/index.d.ts +1 -1
- package/dist/tools/ai-prompts-admin.d.ts +5 -3
- package/dist/tools/ai-providers-admin.d.ts +30 -0
- package/dist/tools/event-catalog.d.ts +20 -0
- package/dist/tools/experiments.d.ts +24 -0
- package/dist/tools/functions.d.ts +1 -1
- package/dist/tools/leagues.d.ts +12 -0
- package/dist/tools/operations.d.ts +24 -0
- package/dist/tools/promotion.d.ts +28 -0
- package/package.json +1 -1
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() {
|
|
@@ -308,7 +308,9 @@ const READ_VERBS = new Set([
|
|
|
308
308
|
"tiers",
|
|
309
309
|
"verify",
|
|
310
310
|
"logs",
|
|
311
|
-
"query"
|
|
311
|
+
"query",
|
|
312
|
+
"aggregate",
|
|
313
|
+
"results"
|
|
312
314
|
]);
|
|
313
315
|
/**
|
|
314
316
|
* Destructive verbs. A tool whose name contains one of these MAY perform
|
|
@@ -368,7 +370,8 @@ const WRITE_VERBS = new Set([
|
|
|
368
370
|
"purchase",
|
|
369
371
|
"signup",
|
|
370
372
|
"login",
|
|
371
|
-
"refresh"
|
|
373
|
+
"refresh",
|
|
374
|
+
"track"
|
|
372
375
|
]);
|
|
373
376
|
/**
|
|
374
377
|
* Split a tool name into lowercase verb-candidate tokens, dropping the
|
|
@@ -384,7 +387,17 @@ function toolNameTokens(name) {
|
|
|
384
387
|
* test suite asserts it doesn't, so a `null` at runtime means a new tool
|
|
385
388
|
* used an unrecognised verb and needs a verb-set entry here.
|
|
386
389
|
*/
|
|
390
|
+
/**
|
|
391
|
+
* Per-tool classification overrides for names whose verb token is a noun
|
|
392
|
+
* elsewhere. `catalog` is a NOUN in catalog-management tools
|
|
393
|
+
* (`amba_create_catalog_item`, `amba_set_item_price`), so it can't be a global
|
|
394
|
+
* read verb — but `amba_events_catalog` is a pure read/discovery tool. Override
|
|
395
|
+
* it explicitly rather than polluting the verb sets.
|
|
396
|
+
*/
|
|
397
|
+
const VERB_CLASS_OVERRIDES = { amba_events_catalog: "read" };
|
|
387
398
|
function classifyToolVerb(name) {
|
|
399
|
+
const override = VERB_CLASS_OVERRIDES[name];
|
|
400
|
+
if (override) return override;
|
|
388
401
|
const tokens = toolNameTokens(name);
|
|
389
402
|
let sawRead = false;
|
|
390
403
|
let sawWrite = false;
|
|
@@ -611,7 +624,7 @@ function registerPublicTool(server, name, description, schema, handler, aliases
|
|
|
611
624
|
}
|
|
612
625
|
//#endregion
|
|
613
626
|
//#region src/tools/projects.ts
|
|
614
|
-
function registerTools$
|
|
627
|
+
function registerTools$35(server, apiClient) {
|
|
615
628
|
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 }) => {
|
|
616
629
|
const result = await client.get("/projects");
|
|
617
630
|
return { content: [{
|
|
@@ -629,7 +642,7 @@ function registerTools$29(server, apiClient) {
|
|
|
629
642
|
registerTool(server, apiClient, "amba_projects_create", "Create a new Amba project. A project represents a mobile app and contains all its engagement configuration (push, segments, config, content, streaks).", {
|
|
630
643
|
name: z.string().describe("Human-readable project name (e.g. \"My Fitness App\")"),
|
|
631
644
|
bundle_id: z.string().optional().describe("App bundle identifier (e.g. \"com.example.myapp\"). Doubles as the audience the server expects on Apple Sign In identity tokens — set this if your app uses Sign in with Apple, or sign-in will reject with AUDIENCE_NOT_CONFIGURED."),
|
|
632
|
-
google_oauth_client_id: z.string().optional().describe("Google OAuth 2.0 client id from Google
|
|
645
|
+
google_oauth_client_id: z.string().optional().describe("Google OAuth 2.0 client id from your Google OAuth configuration. Doubles as the audience the server expects on Google Sign In id tokens — set this if your app uses Sign in with Google, or sign-in will reject with AUDIENCE_NOT_CONFIGURED. The client id is a public identifier (not a secret) — same value that ships in the app binary."),
|
|
633
646
|
platform: z.enum([
|
|
634
647
|
"ios",
|
|
635
648
|
"android",
|
|
@@ -718,7 +731,7 @@ function registerTools$29(server, apiClient) {
|
|
|
718
731
|
}
|
|
719
732
|
//#endregion
|
|
720
733
|
//#region src/tools/push.ts
|
|
721
|
-
function registerTools$
|
|
734
|
+
function registerTools$34(server, apiClient) {
|
|
722
735
|
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.", {
|
|
723
736
|
project_id: z.string().describe("The project ID"),
|
|
724
737
|
title: z.string().describe("Push notification title shown to the user"),
|
|
@@ -848,7 +861,7 @@ const segmentRulesSchema = z.object({
|
|
|
848
861
|
operator: z.enum(["AND", "OR"]).describe("Logical operator combining conditions"),
|
|
849
862
|
conditions: z.array(segmentConditionSchema).describe("Array of filter conditions")
|
|
850
863
|
});
|
|
851
|
-
function registerTools$
|
|
864
|
+
function registerTools$33(server, apiClient) {
|
|
852
865
|
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\").", {
|
|
853
866
|
project_id: z.string().describe("The project ID"),
|
|
854
867
|
include_system: z.boolean().optional().describe("Include built-in system segments in the result. Defaults to false.")
|
|
@@ -933,7 +946,7 @@ const configConditionSchema = z.object({
|
|
|
933
946
|
percentage: z.number().optional().describe("Percentage rollout (0-100)"),
|
|
934
947
|
value: z.unknown().describe("Override value for this condition")
|
|
935
948
|
});
|
|
936
|
-
function registerTools$
|
|
949
|
+
function registerTools$32(server, apiClient) {
|
|
937
950
|
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).", {
|
|
938
951
|
project_id: z.string().describe("The project ID"),
|
|
939
952
|
include_system: z.boolean().optional().describe("Include platform-provided default config keys in the result. Defaults to false.")
|
|
@@ -1011,8 +1024,8 @@ const contentItemSchema = z.object({
|
|
|
1011
1024
|
metadata: z.record(z.unknown()).optional().describe("Arbitrary metadata key-value pairs"),
|
|
1012
1025
|
is_premium: z.boolean().optional().describe("Whether this content requires a premium entitlement")
|
|
1013
1026
|
});
|
|
1014
|
-
function registerTools$
|
|
1015
|
-
registerTool(server, apiClient, "amba_content_libraries_create", "Create a content library for a project. A content library is a collection of content items (tips, quotes, articles) that can be delivered on a schedule to keep users engaged.", {
|
|
1027
|
+
function registerTools$31(server, apiClient) {
|
|
1028
|
+
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`.", {
|
|
1016
1029
|
project_id: z.string().describe("The project ID"),
|
|
1017
1030
|
name: z.string().describe("Library name (e.g. \"Daily Motivation\", \"Workout Tips\")"),
|
|
1018
1031
|
description: z.string().optional().describe("Description of the library content"),
|
|
@@ -1027,6 +1040,17 @@ function registerTools$25(server, apiClient) {
|
|
|
1027
1040
|
text: JSON.stringify(result, null, 2)
|
|
1028
1041
|
}] };
|
|
1029
1042
|
}, ["amba_create_content_library"]);
|
|
1043
|
+
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.", {
|
|
1044
|
+
project_id: z.string().describe("The project ID"),
|
|
1045
|
+
library_id: z.string().describe("The content library ID to delete"),
|
|
1046
|
+
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).")
|
|
1047
|
+
}, async ({ project_id, library_id, cascade }, { client }) => {
|
|
1048
|
+
const result = await client.delete(`/projects/${project_id}/content/libraries/${library_id}`, cascade ? { cascade: "true" } : void 0);
|
|
1049
|
+
return { content: [{
|
|
1050
|
+
type: "text",
|
|
1051
|
+
text: JSON.stringify(result, null, 2)
|
|
1052
|
+
}] };
|
|
1053
|
+
});
|
|
1030
1054
|
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.", {
|
|
1031
1055
|
project_id: z.string().describe("The project ID"),
|
|
1032
1056
|
library_id: z.string().describe("The content library ID"),
|
|
@@ -1056,21 +1080,23 @@ function registerTools$25(server, apiClient) {
|
|
|
1056
1080
|
"random",
|
|
1057
1081
|
"sequential"
|
|
1058
1082
|
]).describe("How content items are selected for delivery"),
|
|
1059
|
-
config: z.record(z.unknown()).optional().describe("Schedule-specific configuration (e.g. delivery time, timezone)")
|
|
1060
|
-
|
|
1083
|
+
config: z.record(z.unknown()).optional().describe("Schedule-specific configuration (e.g. delivery time, timezone)"),
|
|
1084
|
+
batch_size: z.number().int().min(1).max(100).optional().describe("How many items to publish per interval. Defaults to 1 (one item per run). Set to N to publish N items each interval — e.g. \"today's 3 tips\" — without hand-rolling a date-wrap rotation client-side. The selected items are delivered as an array the in-app SDK reads.")
|
|
1085
|
+
}, async ({ project_id, library_id, name, schedule_type, config, batch_size }, { client }) => {
|
|
1061
1086
|
const payload = {
|
|
1062
1087
|
library_id,
|
|
1063
1088
|
name,
|
|
1064
1089
|
schedule_type
|
|
1065
1090
|
};
|
|
1066
1091
|
if (config !== void 0) payload.config = config;
|
|
1092
|
+
if (batch_size !== void 0) payload.batch_size = batch_size;
|
|
1067
1093
|
const result = await client.post(`/projects/${project_id}/content/schedules`, payload);
|
|
1068
1094
|
return { content: [{
|
|
1069
1095
|
type: "text",
|
|
1070
1096
|
text: JSON.stringify(result, null, 2)
|
|
1071
1097
|
}] };
|
|
1072
1098
|
}, ["amba_create_content_schedule"]);
|
|
1073
|
-
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 }) => {
|
|
1099
|
+
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 }) => {
|
|
1074
1100
|
const result = await client.get(`/projects/${project_id}/content/libraries`);
|
|
1075
1101
|
return { content: [{
|
|
1076
1102
|
type: "text",
|
|
@@ -1137,14 +1163,16 @@ function registerTools$25(server, apiClient) {
|
|
|
1137
1163
|
cron: z.string().optional().describe("Cron expression (5-field) for delivery timing"),
|
|
1138
1164
|
timezone: z.string().optional().describe("IANA timezone (e.g. \"America/Los_Angeles\")"),
|
|
1139
1165
|
segment_id: z.string().nullable().optional().describe("Target segment, or null to broadcast to all users"),
|
|
1140
|
-
is_active: z.boolean().optional().describe("Whether the schedule is active")
|
|
1141
|
-
|
|
1166
|
+
is_active: z.boolean().optional().describe("Whether the schedule is active"),
|
|
1167
|
+
batch_size: z.number().int().min(1).max(100).optional().describe("How many items to publish per interval (1–100). Set to N to switch this schedule to publishing N items each run instead of one.")
|
|
1168
|
+
}, async ({ project_id, schedule_id, name, cron, timezone, segment_id, is_active, batch_size }, { client }) => {
|
|
1142
1169
|
const payload = {};
|
|
1143
1170
|
if (name !== void 0) payload.name = name;
|
|
1144
1171
|
if (cron !== void 0) payload.cron = cron;
|
|
1145
1172
|
if (timezone !== void 0) payload.timezone = timezone;
|
|
1146
1173
|
if (segment_id !== void 0) payload.segment_id = segment_id;
|
|
1147
1174
|
if (is_active !== void 0) payload.is_active = is_active;
|
|
1175
|
+
if (batch_size !== void 0) payload.batch_size = batch_size;
|
|
1148
1176
|
const result = await client.patch(`/projects/${project_id}/content/schedules/${schedule_id}`, payload);
|
|
1149
1177
|
return { content: [{
|
|
1150
1178
|
type: "text",
|
|
@@ -1175,7 +1203,7 @@ function registerTools$25(server, apiClient) {
|
|
|
1175
1203
|
}
|
|
1176
1204
|
//#endregion
|
|
1177
1205
|
//#region src/tools/streaks.ts
|
|
1178
|
-
function registerTools$
|
|
1206
|
+
function registerTools$30(server, apiClient) {
|
|
1179
1207
|
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.", {
|
|
1180
1208
|
project_id: z.string().describe("The project ID"),
|
|
1181
1209
|
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`."),
|
|
@@ -1267,11 +1295,11 @@ function registerTools$24(server, apiClient) {
|
|
|
1267
1295
|
//#endregion
|
|
1268
1296
|
//#region src/tools/integrations.ts
|
|
1269
1297
|
const providerEnum = z.enum(INTEGRATION_PROVIDERS);
|
|
1270
|
-
function registerTools$
|
|
1271
|
-
registerTool(server, apiClient, "amba_integrations_configure", "Configure (create or replace) a third-party integration for a project. Supported providers: \"apns\" (Apple Push), \"fcm\" (Firebase Cloud Messaging), \"revenuecat\" (subscription management), \"superwall\" (paywall management). Each provider requires specific config fields.", {
|
|
1298
|
+
function registerTools$29(server, apiClient) {
|
|
1299
|
+
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.", {
|
|
1272
1300
|
project_id: z.string().describe("The project ID"),
|
|
1273
1301
|
provider: providerEnum.describe("Integration provider name"),
|
|
1274
|
-
config: z.record(z.unknown()).describe("Provider-specific configuration.
|
|
1302
|
+
config: z.record(z.unknown()).describe("Provider-specific configuration. " + integrationConfigSummary())
|
|
1275
1303
|
}, async ({ project_id, provider, config }, { client }) => {
|
|
1276
1304
|
const result = await client.post(`/projects/${project_id}/integrations`, {
|
|
1277
1305
|
provider,
|
|
@@ -1337,7 +1365,7 @@ const PERIOD_DAYS = {
|
|
|
1337
1365
|
"30d": "30",
|
|
1338
1366
|
"90d": "90"
|
|
1339
1367
|
};
|
|
1340
|
-
function registerTools$
|
|
1368
|
+
function registerTools$28(server, apiClient) {
|
|
1341
1369
|
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.", {
|
|
1342
1370
|
project_id: z.string().describe("The project ID"),
|
|
1343
1371
|
period: z.enum([
|
|
@@ -1359,8 +1387,58 @@ function registerTools$22(server, apiClient) {
|
|
|
1359
1387
|
}, ["amba_get_analytics"]);
|
|
1360
1388
|
}
|
|
1361
1389
|
//#endregion
|
|
1390
|
+
//#region src/lib/tool-result.ts
|
|
1391
|
+
/**
|
|
1392
|
+
* Shared MCP tool result helpers.
|
|
1393
|
+
*
|
|
1394
|
+
* Every tool emits the same `{ content: [{ type: 'text', text: <json> }] }`
|
|
1395
|
+
* envelope. Two helpers centralize that:
|
|
1396
|
+
*
|
|
1397
|
+
* - [`jsonResult`] — wraps an arbitrary payload.
|
|
1398
|
+
* - [`passthroughResult`] — flattens an upstream HTTP response
|
|
1399
|
+
* (status + parsed body) into the same envelope. Status is written
|
|
1400
|
+
* LAST so a colliding top-level `status` field in the API response
|
|
1401
|
+
* cannot shadow the HTTP status — agents look at `parsed.status` to
|
|
1402
|
+
* distinguish 2xx from 4xx/5xx.
|
|
1403
|
+
*
|
|
1404
|
+
* Lives in `src/lib/` (vs. `src/tools/_helpers.ts`) to set the same
|
|
1405
|
+
* cross-cutting-helper precedent as `src/lib/with-pat.ts` (task #36).
|
|
1406
|
+
* `tools/*` files stay strictly tool registrations.
|
|
1407
|
+
*/
|
|
1408
|
+
/**
|
|
1409
|
+
* Wire-shape every MCP tool handler returns.
|
|
1410
|
+
*
|
|
1411
|
+
* The MCP SDK's `tool()` callback signature is structurally typed and
|
|
1412
|
+
* carries an open index signature for `_meta` etc. Declaring our return
|
|
1413
|
+
* type as a plain `{ content: [...] }` interface won't satisfy that
|
|
1414
|
+
* structural check — so the helpers' return type is left as the actual
|
|
1415
|
+
* inferred shape (no explicit interface) and consumers rely on the
|
|
1416
|
+
* inference + the SDK's structural compatibility. If we ever want a
|
|
1417
|
+
* named alias, write it as a type-alias over the inferred shape rather
|
|
1418
|
+
* than a closed interface.
|
|
1419
|
+
*/
|
|
1420
|
+
/** Wrap an arbitrary payload as a JSON-text tool result. */
|
|
1421
|
+
function jsonResult$1(payload) {
|
|
1422
|
+
return { content: [{
|
|
1423
|
+
type: "text",
|
|
1424
|
+
text: JSON.stringify(payload, null, 2)
|
|
1425
|
+
}] };
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Flatten an upstream HTTP response into the agent-facing tool payload.
|
|
1429
|
+
* Body fields are spread FIRST so a future top-level `status` key in
|
|
1430
|
+
* the API response cannot shadow the HTTP `status` — agents read
|
|
1431
|
+
* `parsed.status` to distinguish 2xx from 4xx/5xx.
|
|
1432
|
+
*/
|
|
1433
|
+
function passthroughResult(result) {
|
|
1434
|
+
return jsonResult$1({
|
|
1435
|
+
...result.body ?? {},
|
|
1436
|
+
status: result.status
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
//#endregion
|
|
1362
1440
|
//#region src/tools/users.ts
|
|
1363
|
-
function registerTools$
|
|
1441
|
+
function registerTools$27(server, apiClient) {
|
|
1364
1442
|
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.", {
|
|
1365
1443
|
project_id: z.string().describe("The project ID"),
|
|
1366
1444
|
limit: z.number().optional().describe("Maximum number of users to return (default 50)"),
|
|
@@ -1379,6 +1457,30 @@ function registerTools$21(server, apiClient) {
|
|
|
1379
1457
|
text: JSON.stringify(result, null, 2)
|
|
1380
1458
|
}] };
|
|
1381
1459
|
}, ["amba_list_users"]);
|
|
1460
|
+
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.", {
|
|
1461
|
+
project_id: z.string().describe("The project ID"),
|
|
1462
|
+
external_id: z.string().optional().describe("Your stable external identifier for this user (unique). Optional."),
|
|
1463
|
+
anonymous_id: z.string().optional().describe("Anonymous device id (unique). Optional; auto-generated if no identity given."),
|
|
1464
|
+
email: z.string().optional().describe("Email address. Optional."),
|
|
1465
|
+
phone: z.string().optional().describe("Phone number. Optional."),
|
|
1466
|
+
display_name: z.string().optional().describe("Display name. Optional."),
|
|
1467
|
+
avatar_url: z.string().optional().describe("Avatar URL. Optional."),
|
|
1468
|
+
properties: z.record(z.unknown()).optional().describe("Arbitrary user properties (JSON object). Optional.")
|
|
1469
|
+
}, async ({ project_id, external_id, anonymous_id, email, phone, display_name, avatar_url, properties }, { client }) => {
|
|
1470
|
+
const payload = {};
|
|
1471
|
+
if (external_id !== void 0) payload.external_id = external_id;
|
|
1472
|
+
if (anonymous_id !== void 0) payload.anonymous_id = anonymous_id;
|
|
1473
|
+
if (email !== void 0) payload.email = email;
|
|
1474
|
+
if (phone !== void 0) payload.phone = phone;
|
|
1475
|
+
if (display_name !== void 0) payload.display_name = display_name;
|
|
1476
|
+
if (avatar_url !== void 0) payload.avatar_url = avatar_url;
|
|
1477
|
+
if (properties !== void 0) payload.properties = properties;
|
|
1478
|
+
const result = await client.post(`/projects/${project_id}/users`, payload);
|
|
1479
|
+
return { content: [{
|
|
1480
|
+
type: "text",
|
|
1481
|
+
text: JSON.stringify(result, null, 2)
|
|
1482
|
+
}] };
|
|
1483
|
+
}, ["amba_create_user"]);
|
|
1382
1484
|
registerTool(server, apiClient, "amba_users_get", "Get a single app user by id, with their streaks and entitlements joined in.", {
|
|
1383
1485
|
project_id: z.string().describe("The project ID"),
|
|
1384
1486
|
user_id: z.string().describe("The app user ID")
|
|
@@ -1455,6 +1557,23 @@ function registerTools$21(server, apiClient) {
|
|
|
1455
1557
|
text: JSON.stringify(result, null, 2)
|
|
1456
1558
|
}] };
|
|
1457
1559
|
});
|
|
1560
|
+
registerTool(server, apiClient, "amba_users_create_cohort", [
|
|
1561
|
+
"Mint N anonymous app_users at once, all sharing the same properties (and, optionally, the same segment memberships) in a single call.",
|
|
1562
|
+
"The seed-a-test-cohort primitive: use it to populate a leaderboard, dry-run a push segment, or stand up a batch of users tagged `cohort: beta`.",
|
|
1563
|
+
"Each user is anonymous-style (no external identity) — a shared-properties cohort has no per-user identity by definition. Pass `segment_ids` to enrol every minted user into those segments.",
|
|
1564
|
+
"Capped at 500 per call (split larger seeds into multiple calls). Returns `{ user_ids, created_count }`.",
|
|
1565
|
+
"Errors: 400 INVALID_COUNT (out of 1–500) / INVALID_SEGMENT_ID (segment does not exist), 500 COHORT_FAILED."
|
|
1566
|
+
].join(" "), {
|
|
1567
|
+
project_id: z.string().describe("The project ID."),
|
|
1568
|
+
count: z.number().int().min(1).max(500).describe("How many users to mint (1–500). Larger counts return INVALID_COUNT."),
|
|
1569
|
+
properties: z.record(z.unknown()).optional().describe("Properties stamped identically onto every minted user (JSON object). Optional."),
|
|
1570
|
+
segment_ids: z.array(z.string()).optional().describe("Segment UUIDs every minted user is enrolled into. Optional.")
|
|
1571
|
+
}, async ({ project_id, count, properties, segment_ids }, { client }) => {
|
|
1572
|
+
const payload = { count };
|
|
1573
|
+
if (properties !== void 0) payload.properties = properties;
|
|
1574
|
+
if (segment_ids !== void 0) payload.segment_ids = segment_ids;
|
|
1575
|
+
return jsonResult$1(await client.post(`/projects/${project_id}/users/cohort`, payload));
|
|
1576
|
+
});
|
|
1458
1577
|
registerTool(server, apiClient, "amba_users_delete", [
|
|
1459
1578
|
"Hard-delete a single app_user. Pass exactly one of `user_id`",
|
|
1460
1579
|
"(uuid) or `anonymous_id` (string). Returns the deleted id plus",
|
|
@@ -1525,6 +1644,31 @@ function registerTools$21(server, apiClient) {
|
|
|
1525
1644
|
text: JSON.stringify(result, null, 2)
|
|
1526
1645
|
}] };
|
|
1527
1646
|
});
|
|
1647
|
+
registerTool(server, apiClient, "amba_users_reset_user", [
|
|
1648
|
+
"Reset ONE app user to a brand-new state WITHOUT deleting them. Clears",
|
|
1649
|
+
"that single user's gamification (XP + ledger, achievements,",
|
|
1650
|
+
"leaderboard entries, challenges), economy (currency balances +",
|
|
1651
|
+
"transactions, inventory, entitlements), streaks, engagement/telemetry",
|
|
1652
|
+
"events, sessions, onboarding, roles, segment memberships, push tokens",
|
|
1653
|
+
"and their social participation (friendships, group + conversation",
|
|
1654
|
+
"memberships, messages, reviews, feed activity) — but keeps the",
|
|
1655
|
+
"app_users row itself (email, anonymous_id, properties survive). This",
|
|
1656
|
+
"is the scalpel to amba_users_reset_sandbox’s sledgehammer: reset a",
|
|
1657
|
+
"single stuck/test user on a production project without touching a",
|
|
1658
|
+
"seeded cohort. Developer-authored config (currencies, achievement",
|
|
1659
|
+
"definitions, catalog) is never touched — the user goes to zero, the",
|
|
1660
|
+
"project does not change. Pass `delete:true` to ALSO remove the",
|
|
1661
|
+
"app_users row at the end (equivalent to amba_users_delete). Returns a",
|
|
1662
|
+
"per-table summary of what was cleared. 404 USER_NOT_FOUND if no row",
|
|
1663
|
+
"matched."
|
|
1664
|
+
].join(" "), {
|
|
1665
|
+
project_id: z.string().describe("The project ID"),
|
|
1666
|
+
user_id: z.string().describe("The app user UUID to reset."),
|
|
1667
|
+
delete: z.boolean().optional().describe("When true, also delete the app_users row after clearing its state (full removal). Defaults to false — the user is kept and only reset.")
|
|
1668
|
+
}, async ({ project_id, user_id, delete: alsoDelete }, { client }) => {
|
|
1669
|
+
const query = alsoDelete ? { delete: "true" } : void 0;
|
|
1670
|
+
return jsonResult$1(await client.delete(`/projects/${project_id}/users/${user_id}/reset`, query));
|
|
1671
|
+
});
|
|
1528
1672
|
registerTool(server, apiClient, "amba_sessions_analytics", "Get session analytics (DAU, active users, total sessions, average and median session duration) for a project across a rolling window.", {
|
|
1529
1673
|
project_id: z.string().describe("The project ID"),
|
|
1530
1674
|
period: z.enum([
|
|
@@ -1638,12 +1782,18 @@ println("Current streak: \${streak.currentCount}")`,
|
|
|
1638
1782
|
// For Expo apps, use @layers/amba-expo instead.
|
|
1639
1783
|
import { Amba } from '@layers/amba-react-native';
|
|
1640
1784
|
|
|
1641
|
-
// Configure once at app startup
|
|
1785
|
+
// Configure once at app startup. The first authenticated call lazily
|
|
1786
|
+
// establishes an anonymous session, so reads work without a manual sign-in.
|
|
1787
|
+
// Call signInAnonymously() explicitly if you want to control when the
|
|
1788
|
+
// anonymous user is created.
|
|
1642
1789
|
await Amba.configure({
|
|
1643
1790
|
projectId: 'YOUR_PROJECT_ID',
|
|
1644
|
-
|
|
1791
|
+
clientKey: 'YOUR_CLIENT_API_KEY',
|
|
1645
1792
|
});
|
|
1646
1793
|
|
|
1794
|
+
// Optional — establish the session up front instead of lazily:
|
|
1795
|
+
// await Amba.auth.signInAnonymously();
|
|
1796
|
+
|
|
1647
1797
|
// Track events
|
|
1648
1798
|
await Amba.events.track('workout_completed', {
|
|
1649
1799
|
duration_minutes: 30,
|
|
@@ -1665,9 +1815,11 @@ import { Amba } from '@layers/amba-expo';
|
|
|
1665
1815
|
|
|
1666
1816
|
export default function RootLayout() {
|
|
1667
1817
|
useEffect(() => {
|
|
1668
|
-
|
|
1818
|
+
// configure() is async. Reads lazily establish an anonymous session on the
|
|
1819
|
+
// first authenticated call, so no manual sign-in is required to start.
|
|
1820
|
+
void Amba.configure({
|
|
1669
1821
|
projectId: process.env.EXPO_PUBLIC_AMBA_PROJECT_ID!,
|
|
1670
|
-
|
|
1822
|
+
clientKey: 'YOUR_CLIENT_API_KEY',
|
|
1671
1823
|
});
|
|
1672
1824
|
}, []);
|
|
1673
1825
|
|
|
@@ -1733,7 +1885,7 @@ final dailyLimit = await Amba.config.get<int>('daily_limit', defaultValue: 10);
|
|
|
1733
1885
|
final streak = await Amba.streaks.get('daily_login');
|
|
1734
1886
|
print('Current streak: \${streak.currentCount}');`
|
|
1735
1887
|
};
|
|
1736
|
-
function registerTools$
|
|
1888
|
+
function registerTools$26(server, apiClient) {
|
|
1737
1889
|
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([
|
|
1738
1890
|
"ios",
|
|
1739
1891
|
"android",
|
|
@@ -1818,7 +1970,7 @@ const criteriaSchema = z.object({
|
|
|
1818
1970
|
property_key: z.string().optional().describe("User property key (required for property_value type)"),
|
|
1819
1971
|
target_value: z.number().describe("Target value to unlock the achievement")
|
|
1820
1972
|
});
|
|
1821
|
-
function registerTools$
|
|
1973
|
+
function registerTools$25(server, apiClient) {
|
|
1822
1974
|
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.", {
|
|
1823
1975
|
project_id: z.string().describe("The project ID"),
|
|
1824
1976
|
key: z.string().describe("Unique key for this achievement (e.g. \"first_workout\", \"streak_master_7\")"),
|
|
@@ -1923,7 +2075,7 @@ function registerTools$19(server, apiClient) {
|
|
|
1923
2075
|
}
|
|
1924
2076
|
//#endregion
|
|
1925
2077
|
//#region src/tools/challenges.ts
|
|
1926
|
-
function registerTools$
|
|
2078
|
+
function registerTools$24(server, apiClient) {
|
|
1927
2079
|
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.", {
|
|
1928
2080
|
project_id: z.string().describe("The project ID"),
|
|
1929
2081
|
name: z.string().describe("Challenge name (e.g. \"7-Day Fitness Sprint\", \"XP Weekend Blitz\")"),
|
|
@@ -2034,7 +2186,7 @@ function registerTools$18(server, apiClient) {
|
|
|
2034
2186
|
}
|
|
2035
2187
|
//#endregion
|
|
2036
2188
|
//#region src/tools/economy.ts
|
|
2037
|
-
function registerTools$
|
|
2189
|
+
function registerTools$23(server, apiClient) {
|
|
2038
2190
|
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.", {
|
|
2039
2191
|
project_id: z.string().describe("The project ID"),
|
|
2040
2192
|
code: z.string().describe("Unique currency code (e.g. \"gold\", \"gems\", \"hearts\")"),
|
|
@@ -2069,38 +2221,42 @@ function registerTools$17(server, apiClient) {
|
|
|
2069
2221
|
text: JSON.stringify(result, null, 2)
|
|
2070
2222
|
}] };
|
|
2071
2223
|
}, ["amba_list_currencies"]);
|
|
2072
|
-
registerTool(server, apiClient, "amba_currencies_grant", "Grant virtual currency to a specific user. Useful for rewards, promotions, or admin adjustments.", {
|
|
2224
|
+
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.", {
|
|
2073
2225
|
project_id: z.string().describe("The project ID"),
|
|
2074
2226
|
app_user_id: z.string().describe("The user to grant currency to"),
|
|
2075
2227
|
currency_code: z.string().describe("Currency code (e.g. \"gold\")"),
|
|
2076
2228
|
amount: z.number().describe("Amount to grant (positive integer)"),
|
|
2077
|
-
reason: z.string().optional().describe("Reason for the grant (for audit trail)")
|
|
2078
|
-
|
|
2229
|
+
reason: z.string().optional().describe("Reason for the grant (for audit trail)"),
|
|
2230
|
+
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).")
|
|
2231
|
+
}, async ({ project_id, app_user_id, currency_code, amount, reason, idempotency_key }, { client }) => {
|
|
2079
2232
|
const payload = {
|
|
2080
2233
|
app_user_id,
|
|
2081
2234
|
currency_code,
|
|
2082
2235
|
amount
|
|
2083
2236
|
};
|
|
2084
2237
|
if (reason !== void 0) payload.reason = reason;
|
|
2238
|
+
if (idempotency_key !== void 0) payload.idempotency_key = idempotency_key;
|
|
2085
2239
|
const result = await client.post(`/projects/${project_id}/currencies/grant`, payload);
|
|
2086
2240
|
return { content: [{
|
|
2087
2241
|
type: "text",
|
|
2088
2242
|
text: JSON.stringify(result, null, 2)
|
|
2089
2243
|
}] };
|
|
2090
2244
|
}, ["amba_grant_currency"]);
|
|
2091
|
-
registerTool(server, apiClient, "amba_currencies_spend", "Debit virtual currency from a specific user. Atomic — returns 400 INSUFFICIENT_FUNDS if the balance is too low (no partial debit). Use for in-app purchases priced in soft currency, gameplay consumes, or admin adjustments. The amount parameter is positive; the ledger records the spend as a negative delta automatically.", {
|
|
2245
|
+
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.", {
|
|
2092
2246
|
project_id: z.string().describe("The project ID"),
|
|
2093
2247
|
app_user_id: z.string().describe("The user to debit currency from"),
|
|
2094
2248
|
currency_code: z.string().describe("Currency code (e.g. \"gold\")"),
|
|
2095
2249
|
amount: z.number().describe("Positive amount to debit. The endpoint records this as a negative delta on the ledger."),
|
|
2096
|
-
reason: z.string().optional().describe("Reason for the spend (stored as reference_id, useful for the audit trail)")
|
|
2097
|
-
|
|
2250
|
+
reason: z.string().optional().describe("Reason for the spend (stored as reference_id, useful for the audit trail)"),
|
|
2251
|
+
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.")
|
|
2252
|
+
}, async ({ project_id, app_user_id, currency_code, amount, reason, idempotency_key }, { client }) => {
|
|
2098
2253
|
const payload = {
|
|
2099
2254
|
app_user_id,
|
|
2100
2255
|
currency_code,
|
|
2101
2256
|
amount
|
|
2102
2257
|
};
|
|
2103
2258
|
if (reason !== void 0) payload.reason = reason;
|
|
2259
|
+
if (idempotency_key !== void 0) payload.idempotency_key = idempotency_key;
|
|
2104
2260
|
const result = await client.post(`/projects/${project_id}/currencies/spend`, payload);
|
|
2105
2261
|
return { content: [{
|
|
2106
2262
|
type: "text",
|
|
@@ -2278,6 +2434,44 @@ function registerTools$17(server, apiClient) {
|
|
|
2278
2434
|
text: JSON.stringify(result, null, 2)
|
|
2279
2435
|
}] };
|
|
2280
2436
|
});
|
|
2437
|
+
registerTool(server, apiClient, "amba_entitlements_grant", [
|
|
2438
|
+
"Grant or refresh a subscription entitlement directly on a user — the same `user_entitlements` row mobile store webhooks (Apple/Play) write to, so a server-side grant and an in-app purchase land in one unified place your client SDK reads with `Amba.entitlements`.",
|
|
2439
|
+
"Covers the non-mobile cases that have no webhook: Stripe/Paddle web checkout, manual support grants, gift codes, legacy migrations. Set is_active=false to revoke (ToS violations, refunds).",
|
|
2440
|
+
"Upserts on (user, entitlement_id) — a second call for the same pair refreshes the existing row in place rather than duplicating. Omit a nullable field to preserve it; pass null to clear it."
|
|
2441
|
+
].join(" "), {
|
|
2442
|
+
project_id: z.string().describe("The project ID"),
|
|
2443
|
+
app_user_id: z.string().describe("The app_user UUID to grant the entitlement to"),
|
|
2444
|
+
entitlement_id: z.string().describe("Entitlement identifier (e.g. \"premium\", \"pro_annual\"). Required."),
|
|
2445
|
+
is_active: z.boolean().optional().describe("Whether the entitlement is active. Defaults to true on grant; pass false to revoke."),
|
|
2446
|
+
product_id: z.string().nullable().optional().describe("Store product identifier (e.g. \"stripe_premium_yr\"). Pass null to clear."),
|
|
2447
|
+
store: z.enum([
|
|
2448
|
+
"app_store",
|
|
2449
|
+
"play_store",
|
|
2450
|
+
"stripe"
|
|
2451
|
+
]).nullable().optional().describe("Originating store. Pass null to clear."),
|
|
2452
|
+
purchase_date: z.string().nullable().optional().describe("ISO-8601 purchase timestamp. Pass null to clear."),
|
|
2453
|
+
expiration_date: z.string().nullable().optional().describe("ISO-8601 expiration timestamp. Pass null to clear (e.g. for a lifetime grant)."),
|
|
2454
|
+
period_type: z.enum([
|
|
2455
|
+
"trial",
|
|
2456
|
+
"intro",
|
|
2457
|
+
"normal"
|
|
2458
|
+
]).nullable().optional().describe("Billing period type. Pass null to clear."),
|
|
2459
|
+
raw_data: z.record(z.unknown()).nullable().optional().describe("Opaque JSON blob for audit (e.g. the raw webhook payload). Pass null to reset to {}.")
|
|
2460
|
+
}, async ({ project_id, app_user_id, entitlement_id, is_active, product_id, store, purchase_date, expiration_date, period_type, raw_data }, { client }) => {
|
|
2461
|
+
const payload = { entitlement_id };
|
|
2462
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
2463
|
+
if (product_id !== void 0) payload.product_id = product_id;
|
|
2464
|
+
if (store !== void 0) payload.store = store;
|
|
2465
|
+
if (purchase_date !== void 0) payload.purchase_date = purchase_date;
|
|
2466
|
+
if (expiration_date !== void 0) payload.expiration_date = expiration_date;
|
|
2467
|
+
if (period_type !== void 0) payload.period_type = period_type;
|
|
2468
|
+
if (raw_data !== void 0) payload.raw_data = raw_data;
|
|
2469
|
+
const result = await client.post(`/projects/${project_id}/users/${app_user_id}/entitlements`, payload);
|
|
2470
|
+
return { content: [{
|
|
2471
|
+
type: "text",
|
|
2472
|
+
text: JSON.stringify(result, null, 2)
|
|
2473
|
+
}] };
|
|
2474
|
+
});
|
|
2281
2475
|
registerTool(server, apiClient, "amba_users_get_inventory", "View a user's inventory including all owned items and quantities.", {
|
|
2282
2476
|
project_id: z.string().describe("The project ID"),
|
|
2283
2477
|
app_user_id: z.string().describe("The user ID to look up")
|
|
@@ -2563,11 +2757,28 @@ function registerTools$17(server, apiClient) {
|
|
|
2563
2757
|
text: JSON.stringify(result, null, 2)
|
|
2564
2758
|
}] };
|
|
2565
2759
|
}, ["amba_get_currency_transactions"]);
|
|
2760
|
+
registerTool(server, apiClient, "amba_currencies_get_user_balance", [
|
|
2761
|
+
"Read a user's current per-currency balances directly — the live value your client SDK and paywall logic see, returned in one call instead of replaying and summing the transaction ledger.",
|
|
2762
|
+
"Returns one row per currency the user holds (balance, lifetime_earned, lifetime_spent, last_recharged_at), or an empty array if they hold none. Pass currency_id to narrow to a single currency.",
|
|
2763
|
+
"Pair with amba_currencies_grant / amba_currencies_spend to verify a balance change landed, or with amba_currencies_get_transactions to reconcile the balance against its ledger history."
|
|
2764
|
+
].join(" "), {
|
|
2765
|
+
project_id: z.string().describe("The project ID"),
|
|
2766
|
+
user_id: z.string().describe("The app user ID"),
|
|
2767
|
+
currency_id: z.string().optional().describe("Filter to one currency (definition UUID).")
|
|
2768
|
+
}, async ({ project_id, user_id, currency_id }, { client }) => {
|
|
2769
|
+
const query = {};
|
|
2770
|
+
if (currency_id !== void 0) query.currency_id = currency_id;
|
|
2771
|
+
const result = await client.get(`/projects/${project_id}/currencies/balances/${user_id}`, query);
|
|
2772
|
+
return { content: [{
|
|
2773
|
+
type: "text",
|
|
2774
|
+
text: JSON.stringify(result, null, 2)
|
|
2775
|
+
}] };
|
|
2776
|
+
});
|
|
2566
2777
|
}
|
|
2567
2778
|
//#endregion
|
|
2568
2779
|
//#region src/tools/leaderboards.ts
|
|
2569
|
-
function registerTools$
|
|
2570
|
-
registerTool(server, apiClient, "amba_leaderboards_create", "Create a leaderboard definition. Leaderboards rank users by XP, streak length, or a custom metric. Supports all-time, daily, weekly, and monthly periods with configurable max entries.", {
|
|
2780
|
+
function registerTools$22(server, apiClient) {
|
|
2781
|
+
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)`.", {
|
|
2571
2782
|
project_id: z.string().describe("The project ID"),
|
|
2572
2783
|
name: z.string().describe("Leaderboard name (e.g. \"Top XP Earners\", \"Weekly Streak Leaders\")"),
|
|
2573
2784
|
metric: z.enum([
|
|
@@ -2612,7 +2823,7 @@ function registerTools$16(server, apiClient) {
|
|
|
2612
2823
|
text: JSON.stringify(result, null, 2)
|
|
2613
2824
|
}] };
|
|
2614
2825
|
}, ["amba_get_leaderboard"]);
|
|
2615
|
-
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 }) => {
|
|
2826
|
+
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 }) => {
|
|
2616
2827
|
const result = await client.get(`/projects/${project_id}/leaderboards`);
|
|
2617
2828
|
return { content: [{
|
|
2618
2829
|
type: "text",
|
|
@@ -2671,8 +2882,80 @@ function registerTools$16(server, apiClient) {
|
|
|
2671
2882
|
}, ["amba_delete_leaderboard"]);
|
|
2672
2883
|
}
|
|
2673
2884
|
//#endregion
|
|
2885
|
+
//#region src/tools/leagues.ts
|
|
2886
|
+
/**
|
|
2887
|
+
* Leagues — the Duolingo-style promote/demote tiers (Bronze/Silver/Gold/…).
|
|
2888
|
+
* Distinct from leaderboards (a single ranked list): a league has weekly
|
|
2889
|
+
* cohorts that the rollover workflow reshuffles. These tools provision and
|
|
2890
|
+
* inspect leagues; the admin routes are mounted at
|
|
2891
|
+
* `/v1/admin/projects/:projectId/leagues`. Members are assigned by the weekly
|
|
2892
|
+
* rollover (the scheduled weekly rollover) and score live off `xp_awarded`
|
|
2893
|
+
* engagement events.
|
|
2894
|
+
*/
|
|
2895
|
+
function registerTools$21(server, apiClient) {
|
|
2896
|
+
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.", {
|
|
2897
|
+
project_id: z.string().describe("The project ID"),
|
|
2898
|
+
name: z.string().describe("League tier name, e.g. \"Bronze\", \"Silver\", \"Gold\""),
|
|
2899
|
+
tier_order: z.number().describe("Tier rank, 1 = lowest/entry tier. Higher tiers have higher tier_order."),
|
|
2900
|
+
promote_count: z.number().optional().describe("How many top members of a cohort promote each week (default 5)"),
|
|
2901
|
+
demote_count: z.number().optional().describe("How many bottom members of a cohort demote each week (default 5)"),
|
|
2902
|
+
cohort_size: z.number().optional().describe("Target members per weekly cohort (default 30)")
|
|
2903
|
+
}, async ({ project_id, name, tier_order, promote_count, demote_count, cohort_size }, { client }) => {
|
|
2904
|
+
const payload = {
|
|
2905
|
+
name,
|
|
2906
|
+
tier_order
|
|
2907
|
+
};
|
|
2908
|
+
if (promote_count !== void 0) payload.promote_count = promote_count;
|
|
2909
|
+
if (demote_count !== void 0) payload.demote_count = demote_count;
|
|
2910
|
+
if (cohort_size !== void 0) payload.cohort_size = cohort_size;
|
|
2911
|
+
const result = await client.post(`/projects/${project_id}/leagues`, payload);
|
|
2912
|
+
return { content: [{
|
|
2913
|
+
type: "text",
|
|
2914
|
+
text: JSON.stringify(result, null, 2)
|
|
2915
|
+
}] };
|
|
2916
|
+
}, ["amba_create_league"]);
|
|
2917
|
+
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 }) => {
|
|
2918
|
+
const result = await client.get(`/projects/${project_id}/leagues`);
|
|
2919
|
+
return { content: [{
|
|
2920
|
+
type: "text",
|
|
2921
|
+
text: JSON.stringify(result, null, 2)
|
|
2922
|
+
}] };
|
|
2923
|
+
}, ["amba_list_leagues"]);
|
|
2924
|
+
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).", {
|
|
2925
|
+
project_id: z.string().describe("The project ID"),
|
|
2926
|
+
league_id: z.string().describe("The league ID"),
|
|
2927
|
+
name: z.string().optional().describe("New tier name"),
|
|
2928
|
+
promote_count: z.number().optional().describe("New weekly promote count"),
|
|
2929
|
+
demote_count: z.number().optional().describe("New weekly demote count"),
|
|
2930
|
+
cohort_size: z.number().optional().describe("New target cohort size"),
|
|
2931
|
+
is_active: z.boolean().optional().describe("Enable/disable the tier")
|
|
2932
|
+
}, async ({ project_id, league_id, name, promote_count, demote_count, cohort_size, is_active }, { client }) => {
|
|
2933
|
+
const payload = {};
|
|
2934
|
+
if (name !== void 0) payload.name = name;
|
|
2935
|
+
if (promote_count !== void 0) payload.promote_count = promote_count;
|
|
2936
|
+
if (demote_count !== void 0) payload.demote_count = demote_count;
|
|
2937
|
+
if (cohort_size !== void 0) payload.cohort_size = cohort_size;
|
|
2938
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
2939
|
+
const result = await client.patch(`/projects/${project_id}/leagues/${league_id}`, payload);
|
|
2940
|
+
return { content: [{
|
|
2941
|
+
type: "text",
|
|
2942
|
+
text: JSON.stringify(result, null, 2)
|
|
2943
|
+
}] };
|
|
2944
|
+
}, ["amba_update_league"]);
|
|
2945
|
+
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.", {
|
|
2946
|
+
project_id: z.string().describe("The project ID"),
|
|
2947
|
+
league_id: z.string().describe("The league ID")
|
|
2948
|
+
}, async ({ project_id, league_id }, { client }) => {
|
|
2949
|
+
const result = await client.get(`/projects/${project_id}/leagues/${league_id}/cohorts/current`);
|
|
2950
|
+
return { content: [{
|
|
2951
|
+
type: "text",
|
|
2952
|
+
text: JSON.stringify(result, null, 2)
|
|
2953
|
+
}] };
|
|
2954
|
+
}, ["amba_list_league_cohorts"]);
|
|
2955
|
+
}
|
|
2956
|
+
//#endregion
|
|
2674
2957
|
//#region src/tools/platform.ts
|
|
2675
|
-
function registerTools$
|
|
2958
|
+
function registerTools$20(server, apiClient) {
|
|
2676
2959
|
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).", {
|
|
2677
2960
|
project_id: z.string().describe("The project ID"),
|
|
2678
2961
|
name: z.string().describe("Flow name (e.g. \"Welcome Flow\", \"Premium Onboarding\")"),
|
|
@@ -3248,7 +3531,7 @@ function registerTools$15(server, apiClient) {
|
|
|
3248
3531
|
}
|
|
3249
3532
|
//#endregion
|
|
3250
3533
|
//#region src/tools/social.ts
|
|
3251
|
-
function registerTools$
|
|
3534
|
+
function registerTools$19(server, apiClient) {
|
|
3252
3535
|
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 }) => {
|
|
3253
3536
|
const result = await client.get(`/projects/${project_id}/friends/stats`);
|
|
3254
3537
|
return { content: [{
|
|
@@ -3573,7 +3856,7 @@ function registerTools$14(server, apiClient) {
|
|
|
3573
3856
|
}
|
|
3574
3857
|
//#endregion
|
|
3575
3858
|
//#region src/tools/xp.ts
|
|
3576
|
-
function registerTools$
|
|
3859
|
+
function registerTools$18(server, apiClient) {
|
|
3577
3860
|
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.", {
|
|
3578
3861
|
project_id: z.string().describe("The project ID"),
|
|
3579
3862
|
name: z.string().describe("Rule name (e.g. \"Workout Completed\", \"Daily Login Bonus\")"),
|
|
@@ -3696,7 +3979,7 @@ function registerTools$13(server, apiClient) {
|
|
|
3696
3979
|
}
|
|
3697
3980
|
//#endregion
|
|
3698
3981
|
//#region src/tools/events.ts
|
|
3699
|
-
function registerTools$
|
|
3982
|
+
function registerTools$17(server, apiClient) {
|
|
3700
3983
|
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.", {
|
|
3701
3984
|
project_id: z.string().describe("The project ID"),
|
|
3702
3985
|
since: z.string().optional().describe("ISO-8601 lower bound for occurred_at. Defaults to 24h ago."),
|
|
@@ -3736,6 +4019,84 @@ function registerTools$12(server, apiClient) {
|
|
|
3736
4019
|
text: JSON.stringify(result, null, 2)
|
|
3737
4020
|
}] };
|
|
3738
4021
|
});
|
|
4022
|
+
registerTool(server, apiClient, "amba_events_track", [
|
|
4023
|
+
"Track an engagement event on behalf of a user from the server side — the one call that drives the whole reactive pipeline: streak qualification, currency grant rules, XP awards, achievements, segment membership, and any webhooks subscribed to the event all fan out from this single write.",
|
|
4024
|
+
"Use it to dogfood the event→XP→achievement loop without a device: emit the exact event your app will (e.g. \"mission_completed\" with {xp: 100}) and watch the downstream effects land, then read them back with amba_users_get_xp / amba_achievements_list.",
|
|
4025
|
+
"Pass `event_id` for exactly-once semantics — a re-POST with the same id is a silent no-op and never double-credits a streak or grant. Omit `app_user_id` for a project-scope event (no end-user attribution).",
|
|
4026
|
+
"Send one event, or a `batch` of up to the per-request cap, in a single round-trip. Returns `{data: {tracked, inserted, skipped}}` — `skipped` counts idempotent replays."
|
|
4027
|
+
].join(" "), {
|
|
4028
|
+
project_id: z.string().describe("The project ID"),
|
|
4029
|
+
event_name: z.string().describe("Event name (e.g. \"mission_completed\", \"lesson_finished\"). Required for a single event.").optional(),
|
|
4030
|
+
app_user_id: z.string().nullable().optional().describe("The app_user UUID this event belongs to. Omit or pass null for a project-scope event with no end-user attribution."),
|
|
4031
|
+
properties: z.record(z.unknown()).optional().describe("Arbitrary event properties (e.g. {xp: 100, level: 3}). Read by grant/XP rules."),
|
|
4032
|
+
occurred_at: z.string().optional().describe("ISO-8601 timestamp the event occurred. Defaults to now."),
|
|
4033
|
+
event_id: z.string().optional().describe("Idempotency key (≤255 chars). A re-POST with the same id is a silent no-op — no double-credit."),
|
|
4034
|
+
batch: z.array(z.object({
|
|
4035
|
+
event_name: z.string().describe("Event name. Required."),
|
|
4036
|
+
app_user_id: z.string().nullable().optional().describe("The app_user UUID. Omit or pass null for a project-scope event."),
|
|
4037
|
+
properties: z.record(z.unknown()).optional().describe("Arbitrary event properties."),
|
|
4038
|
+
occurred_at: z.string().optional().describe("ISO-8601 occurrence timestamp."),
|
|
4039
|
+
event_id: z.string().optional().describe("Idempotency key for this event.")
|
|
4040
|
+
})).optional().describe("Track many events in one call. When provided, the top-level single-event fields are ignored.")
|
|
4041
|
+
}, async ({ project_id, event_name, app_user_id, properties, occurred_at, event_id, batch }, { client }) => {
|
|
4042
|
+
let payload;
|
|
4043
|
+
if (batch !== void 0) payload = { events: batch };
|
|
4044
|
+
else {
|
|
4045
|
+
const single = { event_name };
|
|
4046
|
+
if (app_user_id !== void 0) single.app_user_id = app_user_id;
|
|
4047
|
+
if (properties !== void 0) single.properties = properties;
|
|
4048
|
+
if (occurred_at !== void 0) single.occurred_at = occurred_at;
|
|
4049
|
+
if (event_id !== void 0) single.event_id = event_id;
|
|
4050
|
+
payload = single;
|
|
4051
|
+
}
|
|
4052
|
+
return jsonResult$1(await client.post(`/projects/${project_id}/events`, payload));
|
|
4053
|
+
});
|
|
4054
|
+
}
|
|
4055
|
+
//#endregion
|
|
4056
|
+
//#region src/tools/event-catalog.ts
|
|
4057
|
+
function registerTools$16(server, apiClient) {
|
|
4058
|
+
registerTool(server, apiClient, "amba_events_catalog", "Discover every event you can subscribe a webhook to — both APP events (inside your app: users, gamification, economy, social, content) and CONTROL-plane lifecycle events (about the project itself: provisioning, deploys, domains, billing). Returns each event's type, plane, payload example, and status (`live` = emitted today, `planned` = catalogued, not yet wired). Production pattern: after standing up a project, call this, then subscribe to the lifecycle events you care about (e.g. `domain.registered`, `site.deploy.completed`) with amba_control_webhooks_create and to app events with amba_webhooks_create. Subscriptions match an exact name, a namespace wildcard (`economy.*`), or all (`*`). Pass plane to filter.", {
|
|
4059
|
+
project_id: z.string().describe("The project ID"),
|
|
4060
|
+
plane: z.enum(["app", "control"]).optional().describe("Filter to one plane. Omit for both.")
|
|
4061
|
+
}, async ({ project_id, plane }, { client }) => {
|
|
4062
|
+
const qs = plane ? `?plane=${plane}` : "";
|
|
4063
|
+
const result = await client.get(`/projects/${project_id}/webhooks/catalog${qs}`);
|
|
4064
|
+
return { content: [{
|
|
4065
|
+
type: "text",
|
|
4066
|
+
text: JSON.stringify(result, null, 2)
|
|
4067
|
+
}] };
|
|
4068
|
+
});
|
|
4069
|
+
registerTool(server, apiClient, "amba_control_webhooks_create", "Subscribe a webhook to CONTROL-plane lifecycle events for this project — provisioning, deploys, domains, billing. Amba sends an HMAC-signed POST to target_url whenever a matching event fires. event_name is an exact event (`project.provisioned`), a namespace wildcard (`domain.*`), or all control events (`*`). The signing secret is returned ONCE — copy it into your receiver. target_url must be https:// (http:// only for localhost). Use amba_events_catalog (plane:'control') to see what's available. For APP events use amba_webhooks_create instead.", {
|
|
4070
|
+
project_id: z.string().describe("The project ID"),
|
|
4071
|
+
event_name: z.string().describe("Control event to subscribe to: exact (`project.provisioned`), wildcard (`domain.*`), or `*`."),
|
|
4072
|
+
target_url: z.string().describe("HTTPS endpoint that receives the signed POST. http:// only for localhost.")
|
|
4073
|
+
}, async ({ project_id, event_name, target_url }, { client }) => {
|
|
4074
|
+
const result = await client.post(`/projects/${project_id}/control-webhooks`, {
|
|
4075
|
+
event_name,
|
|
4076
|
+
target_url
|
|
4077
|
+
});
|
|
4078
|
+
return { content: [{
|
|
4079
|
+
type: "text",
|
|
4080
|
+
text: JSON.stringify(result, null, 2)
|
|
4081
|
+
}] };
|
|
4082
|
+
});
|
|
4083
|
+
registerTool(server, apiClient, "amba_control_webhooks_list", "List this project's control-plane webhook subscriptions (most recent first). Secrets are never included.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
4084
|
+
const result = await client.get(`/projects/${project_id}/control-webhooks`);
|
|
4085
|
+
return { content: [{
|
|
4086
|
+
type: "text",
|
|
4087
|
+
text: JSON.stringify(result, null, 2)
|
|
4088
|
+
}] };
|
|
4089
|
+
});
|
|
4090
|
+
registerTool(server, apiClient, "amba_control_webhooks_delete", "Delete a control-plane webhook subscription by id.", {
|
|
4091
|
+
project_id: z.string().describe("The project ID"),
|
|
4092
|
+
id: z.string().describe("The subscription id")
|
|
4093
|
+
}, async ({ project_id, id }, { client }) => {
|
|
4094
|
+
const result = await client.delete(`/projects/${project_id}/control-webhooks/${id}`);
|
|
4095
|
+
return { content: [{
|
|
4096
|
+
type: "text",
|
|
4097
|
+
text: JSON.stringify(result, null, 2)
|
|
4098
|
+
}] };
|
|
4099
|
+
});
|
|
3739
4100
|
}
|
|
3740
4101
|
//#endregion
|
|
3741
4102
|
//#region src/tools/funnels.ts
|
|
@@ -3763,7 +4124,7 @@ const funnelStepSchema = z.object({
|
|
|
3763
4124
|
event: z.string().describe("Event name to match (engagement event_name)."),
|
|
3764
4125
|
filters: z.array(funnelFilterSchema).optional().describe("Optional property predicates, ANDed together, that an event must satisfy.")
|
|
3765
4126
|
});
|
|
3766
|
-
function registerTools$
|
|
4127
|
+
function registerTools$15(server, apiClient) {
|
|
3767
4128
|
registerTool(server, apiClient, "amba_funnels_list", "List conversion funnels for a project. A funnel is a saved, ordered sequence of steps (event + optional property filters) plus a conversion window. Returns the definitions only — use amba_funnels_query to compute per-step counts.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
3768
4129
|
const result = await client.get(`/projects/${project_id}/funnels`);
|
|
3769
4130
|
return { content: [{
|
|
@@ -3857,56 +4218,6 @@ function registerTools$11(server, apiClient) {
|
|
|
3857
4218
|
});
|
|
3858
4219
|
}
|
|
3859
4220
|
//#endregion
|
|
3860
|
-
//#region src/lib/tool-result.ts
|
|
3861
|
-
/**
|
|
3862
|
-
* Shared MCP tool result helpers.
|
|
3863
|
-
*
|
|
3864
|
-
* Every tool emits the same `{ content: [{ type: 'text', text: <json> }] }`
|
|
3865
|
-
* envelope. Two helpers centralize that:
|
|
3866
|
-
*
|
|
3867
|
-
* - [`jsonResult`] — wraps an arbitrary payload.
|
|
3868
|
-
* - [`passthroughResult`] — flattens an upstream HTTP response
|
|
3869
|
-
* (status + parsed body) into the same envelope. Status is written
|
|
3870
|
-
* LAST so a colliding top-level `status` field in the API response
|
|
3871
|
-
* cannot shadow the HTTP status — agents look at `parsed.status` to
|
|
3872
|
-
* distinguish 2xx from 4xx/5xx.
|
|
3873
|
-
*
|
|
3874
|
-
* Lives in `src/lib/` (vs. `src/tools/_helpers.ts`) to set the same
|
|
3875
|
-
* cross-cutting-helper precedent as `src/lib/with-pat.ts` (task #36).
|
|
3876
|
-
* `tools/*` files stay strictly tool registrations.
|
|
3877
|
-
*/
|
|
3878
|
-
/**
|
|
3879
|
-
* Wire-shape every MCP tool handler returns.
|
|
3880
|
-
*
|
|
3881
|
-
* The MCP SDK's `tool()` callback signature is structurally typed and
|
|
3882
|
-
* carries an open index signature for `_meta` etc. Declaring our return
|
|
3883
|
-
* type as a plain `{ content: [...] }` interface won't satisfy that
|
|
3884
|
-
* structural check — so the helpers' return type is left as the actual
|
|
3885
|
-
* inferred shape (no explicit interface) and consumers rely on the
|
|
3886
|
-
* inference + the SDK's structural compatibility. If we ever want a
|
|
3887
|
-
* named alias, write it as a type-alias over the inferred shape rather
|
|
3888
|
-
* than a closed interface.
|
|
3889
|
-
*/
|
|
3890
|
-
/** Wrap an arbitrary payload as a JSON-text tool result. */
|
|
3891
|
-
function jsonResult$1(payload) {
|
|
3892
|
-
return { content: [{
|
|
3893
|
-
type: "text",
|
|
3894
|
-
text: JSON.stringify(payload, null, 2)
|
|
3895
|
-
}] };
|
|
3896
|
-
}
|
|
3897
|
-
/**
|
|
3898
|
-
* Flatten an upstream HTTP response into the agent-facing tool payload.
|
|
3899
|
-
* Body fields are spread FIRST so a future top-level `status` key in
|
|
3900
|
-
* the API response cannot shadow the HTTP `status` — agents read
|
|
3901
|
-
* `parsed.status` to distinguish 2xx from 4xx/5xx.
|
|
3902
|
-
*/
|
|
3903
|
-
function passthroughResult(result) {
|
|
3904
|
-
return jsonResult$1({
|
|
3905
|
-
...result.body ?? {},
|
|
3906
|
-
status: result.status
|
|
3907
|
-
});
|
|
3908
|
-
}
|
|
3909
|
-
//#endregion
|
|
3910
4221
|
//#region src/tools/auth.ts
|
|
3911
4222
|
async function authFetch(apiClient, options) {
|
|
3912
4223
|
const url = `${apiClient.getApiRoot()}${options.path}`;
|
|
@@ -4029,7 +4340,7 @@ function enrichedAuthResult(result, agentInstructions) {
|
|
|
4029
4340
|
status: result.status
|
|
4030
4341
|
});
|
|
4031
4342
|
}
|
|
4032
|
-
function registerTools$
|
|
4343
|
+
function registerTools$14(server, apiClient) {
|
|
4033
4344
|
registerPublicTool(server, "amba_developer_signup", [
|
|
4034
4345
|
"Create a new Amba developer account. Returns a long-lived Personal Access Token (PAT)",
|
|
4035
4346
|
"plus a real isolated Amba project (provisioning asynchronously), plus ready-to-paste",
|
|
@@ -4163,6 +4474,18 @@ async function adminFetch(apiClient, options) {
|
|
|
4163
4474
|
body
|
|
4164
4475
|
};
|
|
4165
4476
|
}
|
|
4477
|
+
const CATALOG_SHAPE_KEYS = [
|
|
4478
|
+
"is_published",
|
|
4479
|
+
"published_at",
|
|
4480
|
+
"is_public",
|
|
4481
|
+
"published"
|
|
4482
|
+
];
|
|
4483
|
+
function catalogShapeWarning(row) {
|
|
4484
|
+
const hit = CATALOG_SHAPE_KEYS.filter((k) => k in row);
|
|
4485
|
+
if (hit.length === 0) return null;
|
|
4486
|
+
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)";
|
|
4487
|
+
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(" ");
|
|
4488
|
+
}
|
|
4166
4489
|
async function clientFetch(apiClient, options) {
|
|
4167
4490
|
let url = `${apiClient.getApiRoot()}/client${options.path}`;
|
|
4168
4491
|
if (options.query && Object.keys(options.query).length > 0) url += `?${new URLSearchParams(options.query).toString()}`;
|
|
@@ -4200,7 +4523,7 @@ const MISSING_API_KEY_ERROR = jsonResult$1({
|
|
|
4200
4523
|
});
|
|
4201
4524
|
const columnSchema = z.object({
|
|
4202
4525
|
name: z.string().describe("Column name. Must match /^[a-z][a-z0-9_]*$/."),
|
|
4203
|
-
type: z.string().describe("Column type. One of: text, integer, bigint, boolean, jsonb, timestamptz, uuid, numeric,
|
|
4526
|
+
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."),
|
|
4204
4527
|
nullable: z.boolean().optional().describe("Whether the column accepts NULL. Defaults to true."),
|
|
4205
4528
|
default: z.unknown().optional().describe("Default SQL literal (e.g. \"NOW()\", 0, \"''\"). Optional."),
|
|
4206
4529
|
dimensions: z.number().optional().describe("Required for `vector` columns; ignored otherwise.")
|
|
@@ -4226,10 +4549,10 @@ const sdkOrderSchema = z.array(z.object({
|
|
|
4226
4549
|
})).describe("Ordering. `[{column: \"created_at\", direction: \"desc\"}, ...]`.");
|
|
4227
4550
|
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.");
|
|
4228
4551
|
const setSchema = z.record(z.unknown()).describe("Column-value map. Server-managed columns (id, created_at, etc.) are rejected.");
|
|
4229
|
-
function registerTools$
|
|
4552
|
+
function registerTools$13(server, apiClient) {
|
|
4230
4553
|
registerTool(server, apiClient, "amba_collections_create", [
|
|
4231
4554
|
"Create a new collection (schema-first Postgres table) in a project.",
|
|
4232
|
-
"The
|
|
4555
|
+
"The schema change is applied server-side via an async workflow.",
|
|
4233
4556
|
"Authenticates as the developer/agent — pass `pat` (the Personal Access Token returned by amba_developer_signup) or send it as the inbound Bearer.",
|
|
4234
4557
|
"Returns the workflow id + version + status. Failure responses include `details.workflow_id` for debugging.",
|
|
4235
4558
|
"Errors: 400 RESERVED_NAME / INVALID_COLLECTION_SCHEMA / COLLECTION_MIGRATION_FAILED, 401 MISSING_PAT."
|
|
@@ -4302,6 +4625,25 @@ function registerTools$9(server, apiClient) {
|
|
|
4302
4625
|
bearer: pat
|
|
4303
4626
|
}));
|
|
4304
4627
|
}, ["amba_delete_collection"]);
|
|
4628
|
+
registerTool(server, apiClient, "amba_collections_reset_data", [
|
|
4629
|
+
"Reset a collection: empty ALL of its rows in one call WITHOUT dropping the collection — the schema, columns and indexes survive, only the data is cleared. Use this to wipe a fixture/test collection back to empty between runs without re-creating it (use amba_collections_delete to drop the collection itself).",
|
|
4630
|
+
"Hard by default — rows are permanently removed. Pass `hard:false` to soft-delete instead (sets deleted_at so rows stay recoverable).",
|
|
4631
|
+
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4632
|
+
"Returns the number of rows cleared. Errors: 404 COLLECTION_NOT_FOUND if the collection does not exist."
|
|
4633
|
+
].join(" "), {
|
|
4634
|
+
project_id: z.string().describe("The project ID."),
|
|
4635
|
+
name: z.string().describe("Collection name whose rows to clear."),
|
|
4636
|
+
hard: z.boolean().optional().describe("true (default) permanently deletes every row; false soft-deletes (sets deleted_at) so rows remain recoverable.")
|
|
4637
|
+
}, async ({ project_id, name, hard }, { pat }) => {
|
|
4638
|
+
const query = {};
|
|
4639
|
+
if (hard === false) query.hard = "false";
|
|
4640
|
+
return passthroughResult(await adminFetch(apiClient, {
|
|
4641
|
+
method: "DELETE",
|
|
4642
|
+
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/data`,
|
|
4643
|
+
query,
|
|
4644
|
+
bearer: pat
|
|
4645
|
+
}));
|
|
4646
|
+
});
|
|
4305
4647
|
registerTool(server, apiClient, "amba_collections_alter", [
|
|
4306
4648
|
"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).",
|
|
4307
4649
|
"Drop-column is destructive — requires `confirm` to equal the dropped column name.",
|
|
@@ -4349,19 +4691,45 @@ function registerTools$9(server, apiClient) {
|
|
|
4349
4691
|
registerTool(server, apiClient, "amba_admin_insert_row", [
|
|
4350
4692
|
"Insert a row directly into a collection from the developer/agent side — BYPASSES auto-RLS.",
|
|
4351
4693
|
"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).",
|
|
4694
|
+
"For developer-seeded GLOBAL content (question banks, lookup tables, daily content) on a shared:true collection, pass as_system=true to write a row with NO owner (user_id = NULL). Rejected with NOT_SHARED_COLLECTION on a non-shared collection — create it with shared:true (or PATCH relax_user_id:true) first.",
|
|
4695
|
+
"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.",
|
|
4352
4696
|
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4353
|
-
"Errors: 400 INVALID_COLUMN, 404 COLLECTION_NOT_FOUND, 500 CREATE_FAILED."
|
|
4697
|
+
"Errors: 400 INVALID_COLUMN / INVALID_CONFLICT_TARGET / NOT_SHARED_COLLECTION, 404 COLLECTION_NOT_FOUND, 500 CREATE_FAILED."
|
|
4354
4698
|
].join(" "), {
|
|
4355
4699
|
project_id: z.string().describe("The project ID."),
|
|
4356
4700
|
name: z.string().describe("Collection name."),
|
|
4357
|
-
row: z.record(z.unknown()).describe("Column-value map for the new row. Server-managed columns are stripped.")
|
|
4358
|
-
|
|
4359
|
-
|
|
4701
|
+
row: z.record(z.unknown()).describe("Column-value map for the new row. Server-managed columns are stripped."),
|
|
4702
|
+
as_system: z.boolean().optional().describe("Write a developer-owned GLOBAL row (user_id = NULL) on a shared:true collection — for content the whole app reads (question banks, lookup tables) rather than data owned by one end-user. Mutually exclusive with a row.user_id. Rejected on non-shared collections."),
|
|
4703
|
+
on_conflict: z.enum([
|
|
4704
|
+
"error",
|
|
4705
|
+
"ignore",
|
|
4706
|
+
"update"
|
|
4707
|
+
]).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."),
|
|
4708
|
+
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\"."),
|
|
4709
|
+
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.")
|
|
4710
|
+
}, async ({ project_id, name, row, as_system, on_conflict, conflict_target, return_minimal }, { pat }) => {
|
|
4711
|
+
const query = {};
|
|
4712
|
+
if (on_conflict !== void 0) query.on_conflict = on_conflict;
|
|
4713
|
+
if (conflict_target !== void 0) query.conflict_target = conflict_target.join(",");
|
|
4714
|
+
if (return_minimal) query.return = "minimal";
|
|
4715
|
+
const requestBody = as_system ? {
|
|
4716
|
+
...row,
|
|
4717
|
+
as_system: true
|
|
4718
|
+
} : row;
|
|
4719
|
+
const result = await adminFetch(apiClient, {
|
|
4360
4720
|
method: "POST",
|
|
4361
4721
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
4362
|
-
body:
|
|
4722
|
+
body: requestBody,
|
|
4723
|
+
query: Object.keys(query).length > 0 ? query : void 0,
|
|
4363
4724
|
bearer: pat
|
|
4364
|
-
})
|
|
4725
|
+
});
|
|
4726
|
+
const warning = catalogShapeWarning(row);
|
|
4727
|
+
const base = passthroughResult(result);
|
|
4728
|
+
if (warning) base.content.unshift({
|
|
4729
|
+
type: "text",
|
|
4730
|
+
text: warning
|
|
4731
|
+
});
|
|
4732
|
+
return base;
|
|
4365
4733
|
});
|
|
4366
4734
|
registerTool(server, apiClient, "amba_admin_insert_rows", [
|
|
4367
4735
|
"Bulk-insert up to 500 rows into a collection in a single atomic statement — BYPASSES auto-RLS.",
|
|
@@ -4374,20 +4742,22 @@ function registerTools$9(server, apiClient) {
|
|
|
4374
4742
|
project_id: z.string().describe("The project ID."),
|
|
4375
4743
|
name: z.string().describe("Collection name."),
|
|
4376
4744
|
rows: z.array(z.record(z.unknown())).min(1).max(500).describe("1–500 column-value maps. Server-managed columns are stripped per row."),
|
|
4377
|
-
on_conflict: z.enum(["error", "skip"]).optional().describe("Conflict policy. \"error\" (default) fails the batch; \"skip\" ignores conflicts.")
|
|
4378
|
-
|
|
4745
|
+
on_conflict: z.enum(["error", "skip"]).optional().describe("Conflict policy. \"error\" (default) fails the batch; \"skip\" ignores conflicts."),
|
|
4746
|
+
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.")
|
|
4747
|
+
}, async ({ project_id, name, rows, on_conflict, return_minimal }, { pat }) => {
|
|
4379
4748
|
const body = { rows };
|
|
4380
4749
|
if (on_conflict !== void 0) body.on_conflict = on_conflict;
|
|
4381
4750
|
return passthroughResult(await adminFetch(apiClient, {
|
|
4382
4751
|
method: "POST",
|
|
4383
4752
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows:batch`,
|
|
4384
4753
|
body,
|
|
4754
|
+
query: return_minimal ? { return: "minimal" } : void 0,
|
|
4385
4755
|
bearer: pat
|
|
4386
4756
|
}));
|
|
4387
4757
|
});
|
|
4388
4758
|
registerTool(server, apiClient, "amba_admin_list_rows", [
|
|
4389
4759
|
"List rows in a collection from the admin side. BYPASSES auto-RLS — every row in the collection is visible.",
|
|
4390
|
-
"Default behavior
|
|
4760
|
+
"Default behavior EXCLUDES soft-deleted rows; pass `include_deleted=true` to include them (e.g. debugging \"where did the row go\").",
|
|
4391
4761
|
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4392
4762
|
"Supports the server FindQuery DSL via `where` + `order` + `limit` + `offset` + `cursor` + `select`."
|
|
4393
4763
|
].join(" "), {
|
|
@@ -4399,7 +4769,7 @@ function registerTools$9(server, apiClient) {
|
|
|
4399
4769
|
offset: z.number().optional().describe("Offset for pagination."),
|
|
4400
4770
|
cursor: z.string().optional().describe("Opaque cursor returned by a prior page."),
|
|
4401
4771
|
select: z.array(z.string()).optional().describe("Subset of columns to return."),
|
|
4402
|
-
include_deleted: z.boolean().optional().describe("Whether to include soft-deleted rows. Admin default is
|
|
4772
|
+
include_deleted: z.boolean().optional().describe("Whether to include soft-deleted rows. Admin default is FALSE.")
|
|
4403
4773
|
}, async ({ project_id, name, where, order, limit, offset, cursor, select, include_deleted }, { pat }) => {
|
|
4404
4774
|
const findQuery = {};
|
|
4405
4775
|
if (where !== void 0) findQuery.where = where;
|
|
@@ -4416,21 +4786,157 @@ function registerTools$9(server, apiClient) {
|
|
|
4416
4786
|
bearer: pat
|
|
4417
4787
|
}));
|
|
4418
4788
|
});
|
|
4789
|
+
registerTool(server, apiClient, "amba_admin_aggregate_rows", [
|
|
4790
|
+
"Run a group-by aggregation over a collection from the admin side — BYPASSES auto-RLS.",
|
|
4791
|
+
"Supports count / sum / avg / min / max, optionally grouped by one or more columns. count without a column = COUNT(*).",
|
|
4792
|
+
"Default EXCLUDES soft-deleted rows; pass include_deleted=true to include them.",
|
|
4793
|
+
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4794
|
+
"Returns one row per group (or a single row when group_by is omitted), with each aggregation under its alias."
|
|
4795
|
+
].join(" "), {
|
|
4796
|
+
project_id: z.string().describe("The project ID."),
|
|
4797
|
+
name: z.string().describe("Collection name."),
|
|
4798
|
+
select: z.array(z.object({
|
|
4799
|
+
fn: z.enum([
|
|
4800
|
+
"count",
|
|
4801
|
+
"sum",
|
|
4802
|
+
"avg",
|
|
4803
|
+
"min",
|
|
4804
|
+
"max"
|
|
4805
|
+
]),
|
|
4806
|
+
column: z.string().optional().describe("Column to aggregate. Required for sum/avg/min/max; omit for COUNT(*)."),
|
|
4807
|
+
as: z.string().optional().describe("Output alias (defaults to fn or fn_column).")
|
|
4808
|
+
})).min(1).describe("One or more aggregations to compute."),
|
|
4809
|
+
group_by: z.union([z.string(), z.array(z.string())]).optional().describe("Column(s) to group by. Omit for a whole-collection aggregate."),
|
|
4810
|
+
where: adminFindWhereSchema.optional(),
|
|
4811
|
+
include_deleted: z.boolean().optional().describe("Include soft-deleted rows. Default FALSE.")
|
|
4812
|
+
}, async ({ project_id, name, select, group_by, where, include_deleted }, { pat }) => {
|
|
4813
|
+
const body = { select };
|
|
4814
|
+
if (group_by !== void 0) body.group_by = group_by;
|
|
4815
|
+
if (where !== void 0) body.where = where;
|
|
4816
|
+
if (include_deleted !== void 0) body.includeDeleted = include_deleted;
|
|
4817
|
+
return passthroughResult(await adminFetch(apiClient, {
|
|
4818
|
+
method: "POST",
|
|
4819
|
+
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows/aggregate`,
|
|
4820
|
+
body,
|
|
4821
|
+
bearer: pat
|
|
4822
|
+
}));
|
|
4823
|
+
});
|
|
4824
|
+
registerTool(server, apiClient, "amba_admin_update_row", [
|
|
4825
|
+
"Update a single row by id from the developer/agent side — BYPASSES the per-user row scoping the client tools enforce, so it can edit ANY row regardless of which app_user owns it.",
|
|
4826
|
+
"Pass `set` (a column→value map of the fields to write). Server-managed columns (id, created_at, updated_at, deleted_at) are rejected with 400 PROTECTED_COLUMN.",
|
|
4827
|
+
"Compare-and-set (optimistic concurrency): pass `expected` (a column→expected-value map) and the update applies ONLY if the row currently still matches those values — otherwise 409 PRECONDITION_FAILED and nothing is written. Null-safe, so `expected: {status: null}` matches a NULL column. Use this for safe read-modify-write (read the row, then update guarded by the values you read) without a transaction.",
|
|
4828
|
+
"For updating many rows by a filter instead of one id, use amba_admin_bulk_update.",
|
|
4829
|
+
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4830
|
+
"Errors: 400 PROTECTED_COLUMN / INVALID_COLUMN / INVALID_BODY, 404 NOT_FOUND (no such row id), 409 PRECONDITION_FAILED (expected mismatch), 404 COLLECTION_NOT_FOUND."
|
|
4831
|
+
].join(" "), {
|
|
4832
|
+
project_id: z.string().describe("The project ID."),
|
|
4833
|
+
name: z.string().describe("Collection name."),
|
|
4834
|
+
id: z.string().describe("Row UUID to update."),
|
|
4835
|
+
set: setSchema.describe("Column-value map to write. Server-managed columns are rejected."),
|
|
4836
|
+
expected: z.record(z.unknown()).optional().describe("Compare-and-set precondition: column→expected-value map. The update applies only if the row currently matches every entry; otherwise 409 PRECONDITION_FAILED and no write occurs. Null-safe (an entry of null matches a NULL column).")
|
|
4837
|
+
}, async ({ project_id, name, id, set, expected }, { pat }) => {
|
|
4838
|
+
return passthroughResult(await adminFetch(apiClient, {
|
|
4839
|
+
method: "PATCH",
|
|
4840
|
+
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows/${encodeURIComponent(id)}`,
|
|
4841
|
+
body: expected !== void 0 ? {
|
|
4842
|
+
set,
|
|
4843
|
+
expected
|
|
4844
|
+
} : { set },
|
|
4845
|
+
bearer: pat
|
|
4846
|
+
}));
|
|
4847
|
+
});
|
|
4848
|
+
registerTool(server, apiClient, "amba_admin_delete_row", [
|
|
4849
|
+
"Delete a single row by id from the developer/agent side — BYPASSES the per-user row scoping the client tools enforce, so it can delete ANY row regardless of which app_user owns it.",
|
|
4850
|
+
"Soft-delete by default (sets deleted_at = NOW() so the row stays recoverable and still appears in admin_list_rows with include_deleted=true). Pass hard=true for a permanent, irreversible DELETE that removes the row entirely.",
|
|
4851
|
+
"Deleting an already-soft-deleted row returns `already_deleted: true` (200), not an error.",
|
|
4852
|
+
"For deleting many rows by a filter instead of one id, use amba_admin_bulk_delete.",
|
|
4853
|
+
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4854
|
+
"Errors: 404 NOT_FOUND (no such row id), 404 COLLECTION_NOT_FOUND, 400 INVALID_UUID."
|
|
4855
|
+
].join(" "), {
|
|
4856
|
+
project_id: z.string().describe("The project ID."),
|
|
4857
|
+
name: z.string().describe("Collection name."),
|
|
4858
|
+
id: z.string().describe("Row UUID to delete."),
|
|
4859
|
+
hard: z.boolean().optional().describe("false (default) soft-deletes (sets deleted_at so the row stays recoverable); true permanently removes the row (irreversible).")
|
|
4860
|
+
}, async ({ project_id, name, id, hard }, { pat }) => {
|
|
4861
|
+
return passthroughResult(await adminFetch(apiClient, {
|
|
4862
|
+
method: "DELETE",
|
|
4863
|
+
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows/${encodeURIComponent(id)}`,
|
|
4864
|
+
query: hard === true ? { hard: "true" } : void 0,
|
|
4865
|
+
bearer: pat
|
|
4866
|
+
}));
|
|
4867
|
+
});
|
|
4868
|
+
registerTool(server, apiClient, "amba_admin_bulk_update", [
|
|
4869
|
+
"Update EVERY row matching a `where` filter in one statement, from the developer/agent side — BYPASSES the per-user row scoping the client tools enforce (updates rows across all app_users).",
|
|
4870
|
+
"`where` is REQUIRED — a bulk update with no filter (which would touch the entire collection) is rejected with 400 WHERE_REQUIRED. To update a single row by id, use amba_admin_update_row instead.",
|
|
4871
|
+
"`set` is the column→value map applied to every matched row. Server-managed columns are rejected with 400 PROTECTED_COLUMN.",
|
|
4872
|
+
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4873
|
+
"Returns `{data: {updated: <n>, ids: [...]}}`. Errors: 400 WHERE_REQUIRED / PROTECTED_COLUMN / INVALID_COLUMN, 404 COLLECTION_NOT_FOUND."
|
|
4874
|
+
].join(" "), {
|
|
4875
|
+
project_id: z.string().describe("The project ID."),
|
|
4876
|
+
name: z.string().describe("Collection name."),
|
|
4877
|
+
set: setSchema.describe("Column-value map applied to every matched row. Required."),
|
|
4878
|
+
where: adminFindWhereSchema.describe("Server WhereClause selecting which rows to update. REQUIRED — there is no \"update all\" shortcut; pass a filter that matches the rows you intend to change.")
|
|
4879
|
+
}, async ({ project_id, name, set, where }, { pat }) => {
|
|
4880
|
+
return passthroughResult(await adminFetch(apiClient, {
|
|
4881
|
+
method: "PATCH",
|
|
4882
|
+
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
4883
|
+
body: {
|
|
4884
|
+
set,
|
|
4885
|
+
where
|
|
4886
|
+
},
|
|
4887
|
+
bearer: pat
|
|
4888
|
+
}));
|
|
4889
|
+
});
|
|
4890
|
+
registerTool(server, apiClient, "amba_admin_bulk_delete", [
|
|
4891
|
+
"Delete EVERY row matching a `where` filter in one statement, from the developer/agent side — BYPASSES the per-user row scoping the client tools enforce (deletes rows across all app_users).",
|
|
4892
|
+
"`where` is REQUIRED — a bulk delete with no filter is rejected. To delete a single row by id, use amba_admin_delete_row.",
|
|
4893
|
+
"Foot-gun guard: `confirm` MUST equal the exact number of rows the `where` filter currently matches. Preview that count first with amba_admin_list_rows (or the count endpoint), then pass it as `confirm`. If the live count differs (e.g. a concurrent write changed it), the delete aborts with 400 CONFIRMATION_MISMATCH and nothing is removed.",
|
|
4894
|
+
"Soft-delete by default (sets deleted_at). Pass hard=true for a permanent, irreversible DELETE of every matched row.",
|
|
4895
|
+
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4896
|
+
"Returns `{data: {deleted: <n>, ids: [...], mode}}`. Errors: 400 WHERE_REQUIRED / CONFIRMATION_REQUIRED / CONFIRMATION_MISMATCH, 404 COLLECTION_NOT_FOUND."
|
|
4897
|
+
].join(" "), {
|
|
4898
|
+
project_id: z.string().describe("The project ID."),
|
|
4899
|
+
name: z.string().describe("Collection name."),
|
|
4900
|
+
where: adminFindWhereSchema.describe("Server WhereClause selecting which rows to delete. REQUIRED."),
|
|
4901
|
+
confirm: z.number().int().nonnegative().describe("The exact number of rows the `where` filter currently matches (preview via amba_admin_list_rows). The delete proceeds only if the live count equals this; otherwise 400 CONFIRMATION_MISMATCH and nothing is deleted."),
|
|
4902
|
+
hard: z.boolean().optional().describe("false (default) soft-deletes (sets deleted_at, recoverable); true permanently removes every matched row (irreversible).")
|
|
4903
|
+
}, async ({ project_id, name, where, confirm, hard }, { pat }) => {
|
|
4904
|
+
const query = { confirm: String(confirm) };
|
|
4905
|
+
if (hard === true) query.hard = "true";
|
|
4906
|
+
return passthroughResult(await adminFetch(apiClient, {
|
|
4907
|
+
method: "DELETE",
|
|
4908
|
+
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
4909
|
+
body: { where },
|
|
4910
|
+
query,
|
|
4911
|
+
bearer: pat
|
|
4912
|
+
}));
|
|
4913
|
+
});
|
|
4419
4914
|
server.tool("amba_client_insert_row", [
|
|
4420
4915
|
"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.",
|
|
4916
|
+
"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.).",
|
|
4421
4917
|
"Authenticates as the end-user — pass `api_key` (project client X-Api-Key) plus `session_token` (the app_user's Bearer).",
|
|
4422
|
-
"Errors: 400 INVALID_COLUMN, 401 if api_key/session missing, 404 COLLECTION_NOT_FOUND."
|
|
4918
|
+
"Errors: 400 INVALID_COLUMN / INVALID_CONFLICT_TARGET, 401 if api_key/session missing, 404 COLLECTION_NOT_FOUND."
|
|
4423
4919
|
].join(" "), {
|
|
4424
4920
|
name: z.string().describe("Collection name."),
|
|
4425
4921
|
row: z.record(z.unknown()).describe("Column-value map. Server-managed columns and user_id are stripped/overridden."),
|
|
4922
|
+
on_conflict: z.enum([
|
|
4923
|
+
"error",
|
|
4924
|
+
"ignore",
|
|
4925
|
+
"update"
|
|
4926
|
+
]).optional().describe("Conflict policy. \"error\" (default), \"ignore\" (return existing unchanged), or \"update\" (merge). Requires conflict_target."),
|
|
4927
|
+
conflict_target: z.array(z.string()).optional().describe("Unique-index columns to conflict on (e.g. [\"user_id\",\"kind\"]). Required for ignore/update."),
|
|
4426
4928
|
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."),
|
|
4427
4929
|
session_token: z.string().optional().describe("Optional app_user session Bearer token. Required by the route's clientSessionAuth.")
|
|
4428
|
-
}, async ({ name, row, api_key, session_token }) => {
|
|
4930
|
+
}, async ({ name, row, on_conflict, conflict_target, api_key, session_token }) => {
|
|
4429
4931
|
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
4932
|
+
const query = {};
|
|
4933
|
+
if (on_conflict !== void 0) query.on_conflict = on_conflict;
|
|
4934
|
+
if (conflict_target !== void 0) query.conflict_target = conflict_target.join(",");
|
|
4430
4935
|
return passthroughResult(await clientFetch(apiClient, {
|
|
4431
4936
|
method: "POST",
|
|
4432
4937
|
path: `/collections/${encodeURIComponent(name)}`,
|
|
4433
4938
|
body: row,
|
|
4939
|
+
...Object.keys(query).length > 0 && { query },
|
|
4434
4940
|
apiKey: api_key,
|
|
4435
4941
|
...session_token !== void 0 && { sessionToken: session_token }
|
|
4436
4942
|
}));
|
|
@@ -4488,6 +4994,7 @@ function registerTools$9(server, apiClient) {
|
|
|
4488
4994
|
});
|
|
4489
4995
|
server.tool("amba_client_update_row", [
|
|
4490
4996
|
"Update a single row by id. Auto-RLS — only succeeds if the row belongs to the signed-in app_user.",
|
|
4997
|
+
"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.",
|
|
4491
4998
|
"For bulk updates with a where-clause, omit `id` and pass `where` + `set`.",
|
|
4492
4999
|
"Server-managed columns and user_id are rejected with 400 INVALID_COLUMN.",
|
|
4493
5000
|
"Authenticates as the end-user — pass `api_key` + `session_token`."
|
|
@@ -4496,15 +5003,19 @@ function registerTools$9(server, apiClient) {
|
|
|
4496
5003
|
id: z.string().optional().describe("Row UUID for single-row update. Omit to bulk update via `where`."),
|
|
4497
5004
|
where: adminFindWhereSchema.optional().describe("Required for bulk update (when `id` is omitted). `where: {}` matches all rows the user owns."),
|
|
4498
5005
|
set: setSchema.describe("Column-value map to write. Required."),
|
|
5006
|
+
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."),
|
|
4499
5007
|
limit: z.number().optional().describe("Bulk-update cap (default 1000, max 10000). Ignored when `id` is set."),
|
|
4500
5008
|
api_key: z.string().describe("Project client X-Api-Key."),
|
|
4501
5009
|
session_token: z.string().optional().describe("App_user session Bearer. Required by the route.")
|
|
4502
|
-
}, async ({ name, id, where, set, limit, api_key, session_token }) => {
|
|
5010
|
+
}, async ({ name, id, where, set, expected, limit, api_key, session_token }) => {
|
|
4503
5011
|
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
4504
5012
|
if (id !== void 0) return passthroughResult(await clientFetch(apiClient, {
|
|
4505
5013
|
method: "PATCH",
|
|
4506
5014
|
path: `/collections/${encodeURIComponent(name)}/${encodeURIComponent(id)}`,
|
|
4507
|
-
body:
|
|
5015
|
+
body: expected !== void 0 ? {
|
|
5016
|
+
set,
|
|
5017
|
+
expected
|
|
5018
|
+
} : { set },
|
|
4508
5019
|
apiKey: api_key,
|
|
4509
5020
|
...session_token !== void 0 && { sessionToken: session_token }
|
|
4510
5021
|
}));
|
|
@@ -4575,24 +5086,32 @@ function registerTools$9(server, apiClient) {
|
|
|
4575
5086
|
});
|
|
4576
5087
|
server.tool("amba_client_find_rows", [
|
|
4577
5088
|
"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.",
|
|
4578
|
-
"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: {...}}`.",
|
|
4579
|
-
"
|
|
5089
|
+
"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: {...}}`.",
|
|
5090
|
+
"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.",
|
|
5091
|
+
"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).",
|
|
4580
5092
|
"Authenticates as the end-user — pass `api_key` + `session_token`."
|
|
4581
5093
|
].join(" "), {
|
|
4582
5094
|
name: z.string().describe("Collection name."),
|
|
4583
5095
|
filter: sdkFilterSchema.optional(),
|
|
4584
5096
|
order: sdkOrderSchema.optional(),
|
|
5097
|
+
search: z.object({
|
|
5098
|
+
q: z.string().describe("Search text."),
|
|
5099
|
+
columns: z.array(z.string()).min(1).describe("Text columns to search."),
|
|
5100
|
+
fuzzy: z.boolean().optional().describe("Typo-tolerant trigram match + relevance ranking. Default false (substring)."),
|
|
5101
|
+
threshold: z.number().optional().describe("Fuzzy only: similarity threshold 0..1 (default 0.3).")
|
|
5102
|
+
}).optional().describe("Keyword / fuzzy text search across columns."),
|
|
4585
5103
|
limit: z.number().optional().describe("Max rows."),
|
|
4586
5104
|
cursor: z.string().optional().describe("Opaque cursor returned by a prior page."),
|
|
4587
5105
|
select: z.array(z.string()).optional().describe("Subset of columns to return."),
|
|
4588
5106
|
include_deleted: z.boolean().optional().describe("Include soft-deleted rows. Default FALSE."),
|
|
4589
5107
|
api_key: z.string().describe("Project client X-Api-Key."),
|
|
4590
5108
|
session_token: z.string().optional().describe("App_user session Bearer. Required by the route.")
|
|
4591
|
-
}, async ({ name, filter, order, limit, cursor, select, include_deleted, api_key, session_token }) => {
|
|
5109
|
+
}, async ({ name, filter, order, search, limit, cursor, select, include_deleted, api_key, session_token }) => {
|
|
4592
5110
|
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
4593
5111
|
const body = {};
|
|
4594
5112
|
if (filter !== void 0) body.filter = filter;
|
|
4595
5113
|
if (order !== void 0) body.order = order;
|
|
5114
|
+
if (search !== void 0) body.search = search;
|
|
4596
5115
|
if (limit !== void 0) body.limit = limit;
|
|
4597
5116
|
if (cursor !== void 0) body.cursor = cursor;
|
|
4598
5117
|
if (select !== void 0) body.select = select;
|
|
@@ -4649,6 +5168,47 @@ function registerTools$9(server, apiClient) {
|
|
|
4649
5168
|
...session_token !== void 0 && { sessionToken: session_token }
|
|
4650
5169
|
}));
|
|
4651
5170
|
});
|
|
5171
|
+
server.tool("amba_client_aggregate_rows", [
|
|
5172
|
+
"Run a group-by aggregation over a collection as the signed-in app_user — server-side, no rows shipped to the client.",
|
|
5173
|
+
"Supports count / sum / avg / min / max, optionally grouped by one or more columns. count without a column = COUNT(*).",
|
|
5174
|
+
"Auto-RLS — the aggregate covers only rows the app_user owns (plus shared/unowned rows) AND'd with the live deleted_at filter; pass include_deleted=true to include soft-deleted rows.",
|
|
5175
|
+
"Roll a leaderboard, a spend total, a per-category count, or a daily streak summary in one round-trip instead of paging every row. Pairs naturally with amba_client_find_rows for the drill-down.",
|
|
5176
|
+
"Authenticates as the end-user — pass `api_key` + `session_token`.",
|
|
5177
|
+
"Returns `{data: [...]}` — one row per group (or a single row when group_by is omitted), each aggregation under its alias."
|
|
5178
|
+
].join(" "), {
|
|
5179
|
+
name: z.string().describe("Collection name."),
|
|
5180
|
+
select: z.array(z.object({
|
|
5181
|
+
fn: z.enum([
|
|
5182
|
+
"count",
|
|
5183
|
+
"sum",
|
|
5184
|
+
"avg",
|
|
5185
|
+
"min",
|
|
5186
|
+
"max"
|
|
5187
|
+
]),
|
|
5188
|
+
column: z.string().optional().describe("Column to aggregate. Required for sum/avg/min/max; omit for COUNT(*)."),
|
|
5189
|
+
as: z.string().optional().describe("Output alias (defaults to fn or fn_column).")
|
|
5190
|
+
})).min(1).describe("One or more aggregations to compute."),
|
|
5191
|
+
group_by: z.union([z.string(), z.array(z.string())]).optional().describe("Column(s) to group by. Omit for a whole-collection aggregate."),
|
|
5192
|
+
filter: sdkFilterSchema.optional(),
|
|
5193
|
+
where: adminFindWhereSchema.optional(),
|
|
5194
|
+
include_deleted: z.boolean().optional().describe("Include soft-deleted rows. Default FALSE."),
|
|
5195
|
+
api_key: z.string().describe("Project client X-Api-Key."),
|
|
5196
|
+
session_token: z.string().optional().describe("App_user session Bearer. Required by the route.")
|
|
5197
|
+
}, async ({ name, select, group_by, filter, where, include_deleted, api_key, session_token }) => {
|
|
5198
|
+
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
5199
|
+
const body = { select };
|
|
5200
|
+
if (group_by !== void 0) body.group_by = group_by;
|
|
5201
|
+
if (filter !== void 0) body.filter = filter;
|
|
5202
|
+
if (where !== void 0) body.where = where;
|
|
5203
|
+
if (include_deleted !== void 0) body.include_deleted = include_deleted;
|
|
5204
|
+
return passthroughResult(await clientFetch(apiClient, {
|
|
5205
|
+
method: "POST",
|
|
5206
|
+
path: `/collections/${encodeURIComponent(name)}/aggregate`,
|
|
5207
|
+
body,
|
|
5208
|
+
apiKey: api_key,
|
|
5209
|
+
...session_token !== void 0 && { sessionToken: session_token }
|
|
5210
|
+
}));
|
|
5211
|
+
});
|
|
4652
5212
|
}
|
|
4653
5213
|
//#endregion
|
|
4654
5214
|
//#region src/tools/_pat.ts
|
|
@@ -4764,24 +5324,27 @@ function jsonResult(payload) {
|
|
|
4764
5324
|
* of a generic upstream 400.
|
|
4765
5325
|
*/
|
|
4766
5326
|
const MAX_BUNDLE_BYTES = 10 * 1024 * 1024;
|
|
4767
|
-
function registerTools$
|
|
5327
|
+
function registerTools$12(server, apiClient) {
|
|
4768
5328
|
registerTool(server, apiClient, "amba_functions_deploy", [
|
|
4769
5329
|
"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`.",
|
|
4770
5330
|
"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).",
|
|
4771
5331
|
"Optional `rate_limit` declares a per-function rate-limit config (validated server-side). Pass null/omit for no rate limit.",
|
|
5332
|
+
"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.",
|
|
4772
5333
|
"Returns the function deployment row + the public URL (`fn_url`: `https://{project_slug}.fn.amba.host/{name}`)."
|
|
4773
5334
|
].join(" "), {
|
|
4774
5335
|
project_id: z.string().describe("The Amba project ID."),
|
|
4775
5336
|
name: z.string().describe("Function name. Lowercase, /^[a-z][a-z0-9_-]*$/, ≤58 chars."),
|
|
4776
5337
|
code: z.string().describe("Bundled JavaScript module source (one entry file)."),
|
|
4777
|
-
rate_limit: z.unknown().optional().describe("Optional rate-limit config object. Shape: see @layers/amba-shared:RateLimitConfig.")
|
|
4778
|
-
|
|
5338
|
+
rate_limit: z.unknown().optional().describe("Optional rate-limit config object. Shape: see @layers/amba-shared:RateLimitConfig."),
|
|
5339
|
+
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.")
|
|
5340
|
+
}, async ({ project_id, name, code, rate_limit, public: isPublic }, { pat }) => {
|
|
4779
5341
|
const byteLen = Buffer.byteLength(code, "utf8");
|
|
4780
5342
|
if (byteLen > MAX_BUNDLE_BYTES) throw new Error(`Bundle size ${byteLen} bytes exceeds ${MAX_BUNDLE_BYTES}-byte cap (10 MiB).`);
|
|
4781
5343
|
const form = new FormData();
|
|
4782
5344
|
form.append("script", new Blob([code], { type: "application/javascript" }), "bundle.js");
|
|
4783
5345
|
const metadata = { name };
|
|
4784
5346
|
if (rate_limit !== void 0 && rate_limit !== null) metadata.rate_limit = rate_limit;
|
|
5347
|
+
if (isPublic === true) metadata.public = true;
|
|
4785
5348
|
form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }), "metadata.json");
|
|
4786
5349
|
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/functions/deploy`, { formData: form }));
|
|
4787
5350
|
}, ["amba_deploy_function"]);
|
|
@@ -4889,7 +5452,7 @@ function registerTools$8(server, apiClient) {
|
|
|
4889
5452
|
}
|
|
4890
5453
|
//#endregion
|
|
4891
5454
|
//#region src/tools/sites.ts
|
|
4892
|
-
function registerTools$
|
|
5455
|
+
function registerTools$11(server, apiClient) {
|
|
4893
5456
|
registerTool(server, apiClient, "amba_sites_deploy", [
|
|
4894
5457
|
"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.",
|
|
4895
5458
|
"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`.",
|
|
@@ -4981,7 +5544,7 @@ function registerTools$7(server, apiClient) {
|
|
|
4981
5544
|
}
|
|
4982
5545
|
//#endregion
|
|
4983
5546
|
//#region src/tools/domains.ts
|
|
4984
|
-
function registerTools$
|
|
5547
|
+
function registerTools$10(server, apiClient) {
|
|
4985
5548
|
registerTool(server, apiClient, "amba_domains_search", ["Search for available domains to buy through Amba. Pass a keyword or a full domain; returns candidate domains with availability and first-year price (USD).", "Free — searching never costs anything. Use this to help a user pick a name, then `amba_domains_check` to price specific picks and `amba_domains_purchase` to buy."].join(" "), {
|
|
4986
5549
|
project_id: z.string().describe("The Amba project ID."),
|
|
4987
5550
|
query: z.string().describe("A keyword (e.g. \"getunbury\") or a full domain (e.g. \"unbury.com\")."),
|
|
@@ -5036,8 +5599,41 @@ function registerTools$6(server, apiClient) {
|
|
|
5036
5599
|
});
|
|
5037
5600
|
}
|
|
5038
5601
|
//#endregion
|
|
5602
|
+
//#region src/tools/operations.ts
|
|
5603
|
+
function registerTools$9(server, apiClient) {
|
|
5604
|
+
registerTool(server, apiClient, "amba_operations_get", [
|
|
5605
|
+
"Poll an async operation by its operation_id to see whether it has finished.",
|
|
5606
|
+
"Returns the operation `status` (pending | running | succeeded | failed); on `failed`, `failed_reason` says why, and on `succeeded`, `result` holds the outcome.",
|
|
5607
|
+
"Use this after a tool returns an `operation_id` (e.g. amba_domains_purchase): re-call until status is succeeded or failed instead of re-running the action."
|
|
5608
|
+
].join(" "), {
|
|
5609
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5610
|
+
operation_id: z.string().describe("The operation handle id returned by the tool that started the work.")
|
|
5611
|
+
}, async ({ project_id, operation_id }, { pat }) => {
|
|
5612
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/operations/${encodeURIComponent(operation_id)}`));
|
|
5613
|
+
});
|
|
5614
|
+
registerTool(server, apiClient, "amba_operations_list", ["List async operations for a project, newest first. Optionally filter by `kind` (e.g. \"domain_purchase\") and/or `status` (pending | running | succeeded | failed).", "Read-only — useful to find an in-flight operation or review recent ones."].join(" "), {
|
|
5615
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5616
|
+
kind: z.string().optional().describe("Filter to one operation kind, e.g. \"domain_purchase\"."),
|
|
5617
|
+
status: z.enum([
|
|
5618
|
+
"pending",
|
|
5619
|
+
"running",
|
|
5620
|
+
"succeeded",
|
|
5621
|
+
"failed"
|
|
5622
|
+
]).optional().describe("Filter to one lifecycle status."),
|
|
5623
|
+
limit: z.number().int().min(1).max(200).optional().describe("Page size (1-200, default 50)."),
|
|
5624
|
+
offset: z.number().int().min(0).optional().describe("Skip rows (default 0).")
|
|
5625
|
+
}, async ({ project_id, kind, status, limit, offset }, { pat }) => {
|
|
5626
|
+
const query = {};
|
|
5627
|
+
if (kind !== void 0) query.kind = kind;
|
|
5628
|
+
if (status !== void 0) query.status = status;
|
|
5629
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
5630
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
5631
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/operations`, { query }));
|
|
5632
|
+
});
|
|
5633
|
+
}
|
|
5634
|
+
//#endregion
|
|
5039
5635
|
//#region src/tools/secrets.ts
|
|
5040
|
-
function registerTools$
|
|
5636
|
+
function registerTools$8(server, apiClient) {
|
|
5041
5637
|
registerTool(server, apiClient, "amba_secrets_set", [
|
|
5042
5638
|
"Set or rotate a secret. Omit `function` for a PROJECT-WIDE secret (one value visible to every function in the project, including ones deployed later) — set it once instead of repeating per function. Pass `function` to scope the secret to just that function.",
|
|
5043
5639
|
"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).",
|
|
@@ -5107,22 +5703,26 @@ function registerTools$5(server, apiClient) {
|
|
|
5107
5703
|
}
|
|
5108
5704
|
//#endregion
|
|
5109
5705
|
//#region src/tools/ai-prompts-admin.ts
|
|
5110
|
-
const PROVIDER_ENUM = z.enum([
|
|
5706
|
+
const PROVIDER_ENUM$1 = z.enum([
|
|
5707
|
+
"anthropic",
|
|
5708
|
+
"openai",
|
|
5709
|
+
"mistral"
|
|
5710
|
+
]);
|
|
5111
5711
|
const RATE_LIMIT_SHAPE = z.object({
|
|
5112
5712
|
window: z.string().optional().describe("Window length (e.g. \"60s\", \"1m\", \"1h\"). Defaults to \"60s\"."),
|
|
5113
5713
|
max: z.number().int().min(1).optional().describe("Max invocations per window. Defaults to 20."),
|
|
5114
5714
|
key: z.string().optional().describe("Bucketing key. Typically \"user_id\" or \"session_id\". Defaults to \"user_id\".")
|
|
5115
5715
|
}).strict();
|
|
5116
|
-
function registerTools$
|
|
5716
|
+
function registerTools$7(server, apiClient) {
|
|
5117
5717
|
registerTool(server, apiClient, "amba_ai_prompts_create", [
|
|
5118
5718
|
"Register a new AI prompt template on a project. Returns the persisted prompt row (name, version, provider, model, client_invokable).",
|
|
5119
|
-
"Name must match /^[a-z][a-z0-9_-]{0,127}$/. Provider must be \"anthropic\" or \"
|
|
5719
|
+
"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\").",
|
|
5120
5720
|
"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.",
|
|
5121
5721
|
"Re-issuing this tool with an existing `name` will bump the prompt to a new version; prefer `amba_ai_prompts_update` for that intent."
|
|
5122
5722
|
].join(" "), {
|
|
5123
5723
|
project_id: z.string().describe("The Amba project ID."),
|
|
5124
5724
|
name: z.string().describe("Prompt name. /^[a-z][a-z0-9_-]{0,127}$/."),
|
|
5125
|
-
provider: PROVIDER_ENUM.describe("AI provider. Must already be registered
|
|
5725
|
+
provider: PROVIDER_ENUM$1.describe("AI provider. Must already be registered via `amba_ai_providers_set`."),
|
|
5126
5726
|
model: z.string().describe("Provider-native model id."),
|
|
5127
5727
|
system_prompt: z.string().nullable().optional().describe("System prompt sent on every invocation. Omit or null for no system prompt."),
|
|
5128
5728
|
max_tokens: z.number().int().min(1).max(2e5).optional().describe("Max output tokens per invocation. Defaults to 4096."),
|
|
@@ -5152,7 +5752,7 @@ function registerTools$4(server, apiClient) {
|
|
|
5152
5752
|
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(" "), {
|
|
5153
5753
|
project_id: z.string().describe("The Amba project ID."),
|
|
5154
5754
|
name: z.string().describe("Prompt name to update."),
|
|
5155
|
-
provider: PROVIDER_ENUM.describe("AI provider."),
|
|
5755
|
+
provider: PROVIDER_ENUM$1.describe("AI provider."),
|
|
5156
5756
|
model: z.string().describe("Provider-native model id."),
|
|
5157
5757
|
system_prompt: z.string().nullable().optional().describe("System prompt; null/omit to clear."),
|
|
5158
5758
|
max_tokens: z.number().int().min(1).max(2e5).optional().describe("Max output tokens."),
|
|
@@ -5232,6 +5832,179 @@ function registerTools$4(server, apiClient) {
|
|
|
5232
5832
|
}, ["amba_invoke_ai_prompt"]);
|
|
5233
5833
|
}
|
|
5234
5834
|
//#endregion
|
|
5835
|
+
//#region src/tools/ai-providers-admin.ts
|
|
5836
|
+
const PROVIDER_ENUM = z.enum([
|
|
5837
|
+
"anthropic",
|
|
5838
|
+
"openai",
|
|
5839
|
+
"mistral",
|
|
5840
|
+
"gemini"
|
|
5841
|
+
]);
|
|
5842
|
+
function registerTools$6(server, apiClient) {
|
|
5843
|
+
registerTool(server, apiClient, "amba_ai_providers_set", [
|
|
5844
|
+
"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.*`.",
|
|
5845
|
+
"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.",
|
|
5846
|
+
"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.",
|
|
5847
|
+
"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."
|
|
5848
|
+
].join(" "), {
|
|
5849
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5850
|
+
provider: PROVIDER_ENUM.describe("Provider the key belongs to: \"anthropic\", \"openai\", \"mistral\", or \"gemini\"."),
|
|
5851
|
+
api_key: z.string().min(10).describe("The upstream provider API key (e.g. an Anthropic or OpenAI account key).")
|
|
5852
|
+
}, async ({ project_id, provider, api_key }, { pat }) => {
|
|
5853
|
+
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/ai/providers`, { body: {
|
|
5854
|
+
name: provider,
|
|
5855
|
+
api_key
|
|
5856
|
+
} }));
|
|
5857
|
+
});
|
|
5858
|
+
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 }) => {
|
|
5859
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/ai/providers`));
|
|
5860
|
+
});
|
|
5861
|
+
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.", {
|
|
5862
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5863
|
+
provider: PROVIDER_ENUM.describe("Provider to revoke.")
|
|
5864
|
+
}, async ({ project_id, provider }, { pat }) => {
|
|
5865
|
+
return jsonResult(await callWithPat(apiClient, pat, "DELETE", `/projects/${encodeURIComponent(project_id)}/ai/providers/${encodeURIComponent(provider)}`) ?? {
|
|
5866
|
+
name: provider,
|
|
5867
|
+
deleted: true
|
|
5868
|
+
});
|
|
5869
|
+
});
|
|
5870
|
+
}
|
|
5871
|
+
//#endregion
|
|
5872
|
+
//#region src/tools/experiments.ts
|
|
5873
|
+
const variantSchema = z.object({
|
|
5874
|
+
key: z.string().describe("Variant key (e.g. \"control\", \"treatment\"). Unique within the experiment."),
|
|
5875
|
+
weight: z.number().int().min(1).max(1e6).describe("Relative allocation weight (positive integer). Weights need not sum to 100.")
|
|
5876
|
+
});
|
|
5877
|
+
const STATUS_ENUM = z.enum([
|
|
5878
|
+
"active",
|
|
5879
|
+
"paused",
|
|
5880
|
+
"ended"
|
|
5881
|
+
]).describe("active = assigns + exposes new users; paused = serves existing assignments but issues no new ones; ended = read-only.");
|
|
5882
|
+
function registerTools$5(server, apiClient) {
|
|
5883
|
+
registerTool(server, apiClient, "amba_experiments_create", [
|
|
5884
|
+
"Create an A/B experiment: a stable `key`, a display `name`, and >= 2 weighted variants.",
|
|
5885
|
+
"End-users are bucketed into a STICKY variant the first time they request an assignment",
|
|
5886
|
+
"(via the SDK) using a deterministic hash over the variant weights; the bucket then never",
|
|
5887
|
+
"changes even if you re-weight later. The FIRST variant is the control for significance."
|
|
5888
|
+
].join(" "), {
|
|
5889
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5890
|
+
key: z.string().describe("Stable experiment key (the SDK addresses the experiment by this)."),
|
|
5891
|
+
name: z.string().describe("Human-readable experiment name."),
|
|
5892
|
+
description: z.string().nullable().optional().describe("Optional description."),
|
|
5893
|
+
variants: z.array(variantSchema).min(2).describe("At least 2 weighted variants. First is the control."),
|
|
5894
|
+
status: STATUS_ENUM.optional().describe("Initial status. Defaults to \"active\".")
|
|
5895
|
+
}, async ({ project_id, key, name, description, variants, status }, { client }) => {
|
|
5896
|
+
const payload = {
|
|
5897
|
+
key,
|
|
5898
|
+
name,
|
|
5899
|
+
variants
|
|
5900
|
+
};
|
|
5901
|
+
if (description !== void 0) payload.description = description;
|
|
5902
|
+
if (status !== void 0) payload.status = status;
|
|
5903
|
+
const result = await client.post(`/projects/${encodeURIComponent(project_id)}/experiments`, payload);
|
|
5904
|
+
return { content: [{
|
|
5905
|
+
type: "text",
|
|
5906
|
+
text: JSON.stringify(result, null, 2)
|
|
5907
|
+
}] };
|
|
5908
|
+
});
|
|
5909
|
+
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 }) => {
|
|
5910
|
+
const result = await client.get(`/projects/${encodeURIComponent(project_id)}/experiments`);
|
|
5911
|
+
return { content: [{
|
|
5912
|
+
type: "text",
|
|
5913
|
+
text: JSON.stringify(result, null, 2)
|
|
5914
|
+
}] };
|
|
5915
|
+
});
|
|
5916
|
+
registerTool(server, apiClient, "amba_experiments_get", "Get a single experiment by key (name, variants, status, timestamps). 404 if not found.", {
|
|
5917
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5918
|
+
key: z.string().describe("Experiment key.")
|
|
5919
|
+
}, async ({ project_id, key }, { client }) => {
|
|
5920
|
+
const result = await client.get(`/projects/${encodeURIComponent(project_id)}/experiments/${encodeURIComponent(key)}`);
|
|
5921
|
+
return { content: [{
|
|
5922
|
+
type: "text",
|
|
5923
|
+
text: JSON.stringify(result, null, 2)
|
|
5924
|
+
}] };
|
|
5925
|
+
});
|
|
5926
|
+
registerTool(server, apiClient, "amba_experiments_update", [
|
|
5927
|
+
"Update an experiment's name, description, variants, and/or status. Only the supplied",
|
|
5928
|
+
"fields change; variants (when supplied) replace the whole list. Re-weighting NEVER",
|
|
5929
|
+
"re-rolls already-assigned users (assignments are sticky); it only affects users assigned",
|
|
5930
|
+
"after the edit. Set status to \"paused\" to stop new assignments or \"ended\" to freeze it."
|
|
5931
|
+
].join(" "), {
|
|
5932
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5933
|
+
key: z.string().describe("Experiment key to update."),
|
|
5934
|
+
name: z.string().optional().describe("New name."),
|
|
5935
|
+
description: z.string().nullable().optional().describe("New description; null to clear."),
|
|
5936
|
+
variants: z.array(variantSchema).min(2).optional().describe("Replacement variant list (>= 2). First is the control."),
|
|
5937
|
+
status: STATUS_ENUM.optional().describe("New status.")
|
|
5938
|
+
}, async ({ project_id, key, name, description, variants, status }, { client }) => {
|
|
5939
|
+
const payload = {};
|
|
5940
|
+
if (name !== void 0) payload.name = name;
|
|
5941
|
+
if (description !== void 0) payload.description = description;
|
|
5942
|
+
if (variants !== void 0) payload.variants = variants;
|
|
5943
|
+
if (status !== void 0) payload.status = status;
|
|
5944
|
+
const result = await client.patch(`/projects/${encodeURIComponent(project_id)}/experiments/${encodeURIComponent(key)}`, payload);
|
|
5945
|
+
return { content: [{
|
|
5946
|
+
type: "text",
|
|
5947
|
+
text: JSON.stringify(result, null, 2)
|
|
5948
|
+
}] };
|
|
5949
|
+
});
|
|
5950
|
+
registerTool(server, apiClient, "amba_experiments_delete", "Delete an experiment by key. Cascades its assignments + exposures. Hard delete.", {
|
|
5951
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5952
|
+
key: z.string().describe("Experiment key to delete.")
|
|
5953
|
+
}, async ({ project_id, key }, { client }) => {
|
|
5954
|
+
const result = await client.delete(`/projects/${encodeURIComponent(project_id)}/experiments/${encodeURIComponent(key)}`);
|
|
5955
|
+
return { content: [{
|
|
5956
|
+
type: "text",
|
|
5957
|
+
text: JSON.stringify(result ?? {
|
|
5958
|
+
key,
|
|
5959
|
+
deleted: true
|
|
5960
|
+
}, null, 2)
|
|
5961
|
+
}] };
|
|
5962
|
+
});
|
|
5963
|
+
registerTool(server, apiClient, "amba_experiments_results", [
|
|
5964
|
+
"Compute per-variant results for an experiment: assigned_count, exposed_count, and",
|
|
5965
|
+
"(when `conversion_event` is supplied) converted_count + conversion_rate — the distinct",
|
|
5966
|
+
"exposed users who emitted that analytics event at/after their assignment. Each non-control",
|
|
5967
|
+
"variant also gets a two-proportion z-test vs the control (first variant): a z_score and an",
|
|
5968
|
+
"approximate two-sided p_value. The control variant's z_score/p_value are null."
|
|
5969
|
+
].join(" "), {
|
|
5970
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5971
|
+
key: z.string().describe("Experiment key."),
|
|
5972
|
+
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).")
|
|
5973
|
+
}, async ({ project_id, key, conversion_event }, { client }) => {
|
|
5974
|
+
const query = {};
|
|
5975
|
+
if (conversion_event !== void 0) query.conversion_event = conversion_event;
|
|
5976
|
+
const result = await client.get(`/projects/${encodeURIComponent(project_id)}/experiments/${encodeURIComponent(key)}/results`, query);
|
|
5977
|
+
return { content: [{
|
|
5978
|
+
type: "text",
|
|
5979
|
+
text: JSON.stringify(result, null, 2)
|
|
5980
|
+
}] };
|
|
5981
|
+
});
|
|
5982
|
+
}
|
|
5983
|
+
//#endregion
|
|
5984
|
+
//#region src/tools/promotion.ts
|
|
5985
|
+
function registerTools$4(server, apiClient) {
|
|
5986
|
+
registerTool(server, apiClient, "amba_project_export", [
|
|
5987
|
+
"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.",
|
|
5988
|
+
"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.",
|
|
5989
|
+
"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."
|
|
5990
|
+
].join(" "), { project_id: z.string().describe("The Amba project ID to export configuration FROM.") }, async ({ project_id }, { pat }) => {
|
|
5991
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/promotion/export`));
|
|
5992
|
+
});
|
|
5993
|
+
registerTool(server, apiClient, "amba_project_import", [
|
|
5994
|
+
"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).",
|
|
5995
|
+
"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.",
|
|
5996
|
+
"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)."
|
|
5997
|
+
].join(" "), {
|
|
5998
|
+
project_id: z.string().describe("The Amba project ID to import configuration INTO."),
|
|
5999
|
+
bundle: z.record(z.unknown()).describe("The bundle object returned by `amba_project_export` (its `data` field)."),
|
|
6000
|
+
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.")
|
|
6001
|
+
}, async ({ project_id, bundle, mode }, { pat }) => {
|
|
6002
|
+
const body = { bundle };
|
|
6003
|
+
if (mode !== void 0) body.mode = mode;
|
|
6004
|
+
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/promotion/import`, { body }));
|
|
6005
|
+
});
|
|
6006
|
+
}
|
|
6007
|
+
//#endregion
|
|
5235
6008
|
//#region src/tools/billing.ts
|
|
5236
6009
|
/**
|
|
5237
6010
|
* Static tier catalog. Mirrors the marketing-page structure shipped in
|
|
@@ -6993,7 +7766,7 @@ Admin tools authenticate the developer/agent (pass \`pat\` or send it as the inb
|
|
|
6993
7766
|
| \`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 }\` |
|
|
6994
7767
|
| \`amba_collections_list\` | List collections in this project. | \`{ project_id }\` |
|
|
6995
7768
|
| \`amba_collections_get\` | Read one collection's schema. | \`{ project_id, name: "todos" }\` |
|
|
6996
|
-
| \`amba_collections_alter\` | Exactly ONE of: \`add_column\`, \`add_index\`, \`drop_column\`, or \`relax_user_id\` per call. \`relax_user_id: true\` converts an existing collection to shared (drops the \`user_id\` NOT NULL). | \`{ project_id, name: "todos", add_column: { name: "priority", type: "
|
|
7769
|
+
| \`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 } }\` |
|
|
6997
7770
|
| \`amba_collections_delete\` | Drop the table (destructive). \`confirm\` must equal the collection name. | \`{ project_id, name: "todos", confirm: "todos" }\` |
|
|
6998
7771
|
| \`amba_admin_insert_row\` | Insert one row as the developer (bypasses user-scope; \`user_id\` honored if present). | \`{ project_id, name: "todos", row: { title: "Sample", done: false } }\` |
|
|
6999
7772
|
| \`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" }\` |
|
|
@@ -7007,7 +7780,7 @@ Admin tools authenticate the developer/agent (pass \`pat\` or send it as the inb
|
|
|
7007
7780
|
| \`amba_client_find_rows\` | Filter / sort / paginate rows (SDK-shaped \`filter\`). | \`{ api_key, session_token, name: "todos", filter: {...}, order: ["created_at desc"], limit: 50 }\` |
|
|
7008
7781
|
| \`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 }\` |
|
|
7009
7782
|
|
|
7010
|
-
Column types: \`text\`, \`
|
|
7783
|
+
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.
|
|
7011
7784
|
|
|
7012
7785
|
### Functions (serverless code)
|
|
7013
7786
|
|
|
@@ -7027,16 +7800,23 @@ Run user code in a sandbox triggered by HTTP, cron, or webhook. The function get
|
|
|
7027
7800
|
|
|
7028
7801
|
### AI prompts
|
|
7029
7802
|
|
|
7030
|
-
Managed LLM templates: stored prompt with model + system message
|
|
7803
|
+
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.
|
|
7804
|
+
|
|
7805
|
+
**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.
|
|
7806
|
+
|
|
7807
|
+
> 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\`.
|
|
7031
7808
|
|
|
7032
7809
|
| Tool | Purpose | Example args |
|
|
7033
7810
|
| --- | --- | --- |
|
|
7034
|
-
| \`
|
|
7811
|
+
| \`amba_ai_providers_set\` | Register / rotate the upstream provider API key. **Do this first.** | \`{ project_id, provider: "anthropic", api_key: "sk-ant-..." }\` |
|
|
7812
|
+
| \`amba_ai_providers_list\` | List registered providers (\`configured\` = key set). | \`{ project_id }\` |
|
|
7813
|
+
| \`amba_ai_providers_delete\` | Revoke a provider key (fails if prompts still reference it). | \`{ project_id, provider: "anthropic" }\` |
|
|
7814
|
+
| \`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 }\` |
|
|
7035
7815
|
| \`amba_ai_prompts_list\` | List prompts. | \`{ project_id }\` |
|
|
7036
|
-
| \`amba_ai_prompts_get\` | Read one prompt. | \`{ project_id,
|
|
7037
|
-
| \`amba_ai_prompts_update\` | Edit a prompt. | \`{ project_id,
|
|
7038
|
-
| \`amba_ai_prompts_invoke\` | Invoke a prompt server-side (admin testing). | \`{ project_id,
|
|
7039
|
-
| \`amba_ai_prompts_delete\` | Delete. | \`{ project_id,
|
|
7816
|
+
| \`amba_ai_prompts_get\` | Read one prompt. | \`{ project_id, name }\` |
|
|
7817
|
+
| \`amba_ai_prompts_update\` | Edit a prompt (replaces all fields; bumps version). | \`{ project_id, name, provider, model, system_prompt: "..." }\` |
|
|
7818
|
+
| \`amba_ai_prompts_invoke\` | Invoke a prompt server-side (admin testing). \`messages\` is a provider-shaped array. | \`{ project_id, name, messages: [{ role: "user", content: "..." }] }\` |
|
|
7819
|
+
| \`amba_ai_prompts_delete\` | Delete. | \`{ project_id, name }\` |
|
|
7040
7820
|
|
|
7041
7821
|
### Analytics + events + sessions
|
|
7042
7822
|
|
|
@@ -7052,9 +7832,11 @@ Managed LLM templates: stored prompt with model + system message + variables, ca
|
|
|
7052
7832
|
|
|
7053
7833
|
### Secrets + configs + integrations
|
|
7054
7834
|
|
|
7835
|
+
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).
|
|
7836
|
+
|
|
7055
7837
|
| Tool | Purpose | Example args |
|
|
7056
7838
|
| --- | --- | --- |
|
|
7057
|
-
| \`amba_secrets_set\` | Set a
|
|
7839
|
+
| \`amba_secrets_set\` | Set a function-scoped secret (encrypted at rest; bound on the next deploy). | \`{ project_id, name: "STRIPE_WEBHOOK_SECRET", value: "whsec_..." }\` |
|
|
7058
7840
|
| \`amba_secrets_get\` | Read a secret (returns \`"<redacted>"\` unless explicitly requested). | \`{ project_id, name }\` |
|
|
7059
7841
|
| \`amba_secrets_list\` | List secret names. | \`{ project_id }\` |
|
|
7060
7842
|
| \`amba_secrets_delete\` | Delete. | \`{ project_id, name }\` |
|
|
@@ -7113,9 +7895,9 @@ const newTodo = await Amba.collections.insert('todos', { title: 'Ship the app',
|
|
|
7113
7895
|
await Amba.collections.update('todos', newTodo.id, { done: true });
|
|
7114
7896
|
await Amba.collections.delete('todos', newTodo.id);
|
|
7115
7897
|
|
|
7116
|
-
// AI — call a managed prompt
|
|
7898
|
+
// AI — call a managed prompt (prompt_slug names the registered prompt)
|
|
7117
7899
|
const response = await Amba.ai.anthropic.messages.create({
|
|
7118
|
-
|
|
7900
|
+
prompt_slug: 'summarize',
|
|
7119
7901
|
variables: { text: 'A long article about backend services …' },
|
|
7120
7902
|
});
|
|
7121
7903
|
|
|
@@ -7183,8 +7965,7 @@ let showBeta = try await Amba.flags.get(name: "beta_feature")
|
|
|
7183
7965
|
try await Amba.events.track("app_opened", properties: ["source": "deep_link"])
|
|
7184
7966
|
|
|
7185
7967
|
let reply = try await Amba.ai.anthropic.messages.create(
|
|
7186
|
-
|
|
7187
|
-
variables: ["text": "A long article..."]
|
|
7968
|
+
request: AiMessageRequest(promptSlug: "summarize", variables: ["text": "A long article..."])
|
|
7188
7969
|
)
|
|
7189
7970
|
\`\`\`
|
|
7190
7971
|
|
|
@@ -7241,7 +8022,7 @@ Batch.
|
|
|
7241
8022
|
- [ ] Resend (transactional email)
|
|
7242
8023
|
- [ ] Stripe (web payments / subscriptions)
|
|
7243
8024
|
- [ ] Mixpanel / PostHog / Segment (analytics forwarding)
|
|
7244
|
-
- [ ] OpenAI / Anthropic
|
|
8025
|
+
- [ ] OpenAI / Anthropic / Mistral / Gemini LLM keys (required for \`Amba.ai.*\` — set via \`amba_ai_providers_set\`, **not** \`amba_integrations_configure\`)
|
|
7245
8026
|
|
|
7246
8027
|
6. **Feature flags:** seed any starter flags?
|
|
7247
8028
|
- Yes — wire \`beta_feature\` (off by default) so I can ship the wiring before the feature exists
|
|
@@ -7256,7 +8037,7 @@ Batch.
|
|
|
7256
8037
|
1. Before creating:
|
|
7257
8038
|
- \`amba_collections_list\` — match on \`name\`. Collisions: never silently recreate (data loss). Offer \`amba_collections_alter\` to add new columns instead.
|
|
7258
8039
|
- \`amba_functions_list\` — match on \`name\`. Collisions: ask to redeploy (with the new source) or skip.
|
|
7259
|
-
- \`amba_ai_prompts_list\` — match on \`
|
|
8040
|
+
- \`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.)
|
|
7260
8041
|
- \`amba_integrations_list\` — match on \`provider\`. Same.
|
|
7261
8042
|
- \`amba_configs_list\` — match on \`key\`. Same.
|
|
7262
8043
|
|
|
@@ -7438,11 +8219,15 @@ const TOOL_CATEGORY = {
|
|
|
7438
8219
|
amba_developer_rotate_pat: "identity",
|
|
7439
8220
|
amba_users_list: "identity",
|
|
7440
8221
|
amba_list_users: "identity",
|
|
8222
|
+
amba_users_create: "identity",
|
|
8223
|
+
amba_create_user: "identity",
|
|
7441
8224
|
amba_users_get: "identity",
|
|
7442
8225
|
amba_users_delete: "identity",
|
|
7443
8226
|
amba_users_export: "identity",
|
|
7444
8227
|
amba_users_bulk_update: "identity",
|
|
8228
|
+
amba_users_create_cohort: "identity",
|
|
7445
8229
|
amba_users_reset_sandbox: "identity",
|
|
8230
|
+
amba_users_reset_user: "identity",
|
|
7446
8231
|
amba_users_events_export: "identity",
|
|
7447
8232
|
amba_users_list_events: "identity",
|
|
7448
8233
|
amba_api_keys_create: "identity",
|
|
@@ -7486,6 +8271,7 @@ const TOOL_CATEGORY = {
|
|
|
7486
8271
|
amba_onboarding_get_stats: "engagement",
|
|
7487
8272
|
amba_get_onboarding_stats: "engagement",
|
|
7488
8273
|
amba_content_libraries_create: "engagement",
|
|
8274
|
+
amba_content_libraries_delete: "engagement",
|
|
7489
8275
|
amba_create_content_library: "engagement",
|
|
7490
8276
|
amba_content_list_libraries: "engagement",
|
|
7491
8277
|
amba_content_items_add: "engagement",
|
|
@@ -7555,6 +8341,14 @@ const TOOL_CATEGORY = {
|
|
|
7555
8341
|
amba_update_leaderboard: "gamification",
|
|
7556
8342
|
amba_leaderboards_delete: "gamification",
|
|
7557
8343
|
amba_delete_leaderboard: "gamification",
|
|
8344
|
+
amba_leagues_create: "gamification",
|
|
8345
|
+
amba_create_league: "gamification",
|
|
8346
|
+
amba_leagues_list: "gamification",
|
|
8347
|
+
amba_list_leagues: "gamification",
|
|
8348
|
+
amba_leagues_update: "gamification",
|
|
8349
|
+
amba_update_league: "gamification",
|
|
8350
|
+
amba_leagues_list_cohorts: "gamification",
|
|
8351
|
+
amba_list_league_cohorts: "gamification",
|
|
7558
8352
|
amba_challenges_create: "gamification",
|
|
7559
8353
|
amba_create_challenge: "gamification",
|
|
7560
8354
|
amba_challenges_list: "gamification",
|
|
@@ -7580,6 +8374,7 @@ const TOOL_CATEGORY = {
|
|
|
7580
8374
|
amba_currencies_spend: "economy",
|
|
7581
8375
|
amba_currencies_get_transactions: "economy",
|
|
7582
8376
|
amba_get_currency_transactions: "economy",
|
|
8377
|
+
amba_currencies_get_user_balance: "economy",
|
|
7583
8378
|
amba_currency_grant_rules_create: "economy",
|
|
7584
8379
|
amba_currency_grant_rules_list: "economy",
|
|
7585
8380
|
amba_currency_grant_rules_delete: "economy",
|
|
@@ -7613,6 +8408,7 @@ const TOOL_CATEGORY = {
|
|
|
7613
8408
|
amba_stores_delete_listing: "economy",
|
|
7614
8409
|
amba_inventory_grant_item: "economy",
|
|
7615
8410
|
amba_inventory_revoke_item: "economy",
|
|
8411
|
+
amba_entitlements_grant: "economy",
|
|
7616
8412
|
amba_grant_item: "economy",
|
|
7617
8413
|
amba_users_get_inventory: "economy",
|
|
7618
8414
|
amba_get_user_inventory: "economy",
|
|
@@ -7673,6 +8469,7 @@ const TOOL_CATEGORY = {
|
|
|
7673
8469
|
amba_moderation_list_trust: "social",
|
|
7674
8470
|
amba_events_list: "analytics",
|
|
7675
8471
|
amba_events_count: "analytics",
|
|
8472
|
+
amba_events_track: "analytics",
|
|
7676
8473
|
amba_analytics_get: "analytics",
|
|
7677
8474
|
amba_get_analytics: "analytics",
|
|
7678
8475
|
amba_sessions_list: "analytics",
|
|
@@ -7683,6 +8480,14 @@ const TOOL_CATEGORY = {
|
|
|
7683
8480
|
amba_funnels_update: "analytics",
|
|
7684
8481
|
amba_funnels_delete: "analytics",
|
|
7685
8482
|
amba_funnels_query: "analytics",
|
|
8483
|
+
amba_experiments_create: "analytics",
|
|
8484
|
+
amba_experiments_list: "analytics",
|
|
8485
|
+
amba_experiments_get: "analytics",
|
|
8486
|
+
amba_experiments_update: "analytics",
|
|
8487
|
+
amba_experiments_delete: "analytics",
|
|
8488
|
+
amba_experiments_results: "analytics",
|
|
8489
|
+
amba_project_export: "infrastructure",
|
|
8490
|
+
amba_project_import: "infrastructure",
|
|
7686
8491
|
amba_projects_create: "infrastructure",
|
|
7687
8492
|
amba_create_project: "infrastructure",
|
|
7688
8493
|
amba_projects_list: "infrastructure",
|
|
@@ -7707,14 +8512,21 @@ const TOOL_CATEGORY = {
|
|
|
7707
8512
|
amba_alter_collection: "infrastructure",
|
|
7708
8513
|
amba_collections_delete: "infrastructure",
|
|
7709
8514
|
amba_delete_collection: "infrastructure",
|
|
8515
|
+
amba_collections_reset_data: "infrastructure",
|
|
7710
8516
|
amba_admin_insert_row: "infrastructure",
|
|
8517
|
+
amba_admin_update_row: "infrastructure",
|
|
8518
|
+
amba_admin_delete_row: "infrastructure",
|
|
8519
|
+
amba_admin_bulk_update: "infrastructure",
|
|
8520
|
+
amba_admin_bulk_delete: "infrastructure",
|
|
7711
8521
|
amba_admin_list_rows: "infrastructure",
|
|
8522
|
+
amba_admin_aggregate_rows: "infrastructure",
|
|
7712
8523
|
amba_client_insert_row: "infrastructure",
|
|
7713
8524
|
amba_client_list_rows: "infrastructure",
|
|
7714
8525
|
amba_client_get_row: "infrastructure",
|
|
7715
8526
|
amba_client_count_rows: "infrastructure",
|
|
7716
8527
|
amba_client_find_rows: "infrastructure",
|
|
7717
8528
|
amba_client_find_nearest_rows: "infrastructure",
|
|
8529
|
+
amba_client_aggregate_rows: "infrastructure",
|
|
7718
8530
|
amba_client_update_row: "infrastructure",
|
|
7719
8531
|
amba_client_delete_row: "infrastructure",
|
|
7720
8532
|
amba_functions_list: "infrastructure",
|
|
@@ -7753,6 +8565,8 @@ const TOOL_CATEGORY = {
|
|
|
7753
8565
|
amba_domains_check: "infrastructure",
|
|
7754
8566
|
amba_domains_purchase: "infrastructure",
|
|
7755
8567
|
amba_domains_list: "infrastructure",
|
|
8568
|
+
amba_operations_get: "infrastructure",
|
|
8569
|
+
amba_operations_list: "infrastructure",
|
|
7756
8570
|
amba_media_upload: "infrastructure",
|
|
7757
8571
|
amba_upload_media: "infrastructure",
|
|
7758
8572
|
amba_media_list: "infrastructure",
|
|
@@ -7784,6 +8598,9 @@ const TOOL_CATEGORY = {
|
|
|
7784
8598
|
amba_integrations_patch: "infrastructure",
|
|
7785
8599
|
amba_integrations_test: "infrastructure",
|
|
7786
8600
|
amba_test_integration: "infrastructure",
|
|
8601
|
+
amba_ai_providers_set: "infrastructure",
|
|
8602
|
+
amba_ai_providers_list: "infrastructure",
|
|
8603
|
+
amba_ai_providers_delete: "infrastructure",
|
|
7787
8604
|
amba_ai_prompts_create: "infrastructure",
|
|
7788
8605
|
amba_create_ai_prompt: "infrastructure",
|
|
7789
8606
|
amba_ai_prompts_list: "infrastructure",
|
|
@@ -7815,6 +8632,10 @@ const TOOL_CATEGORY = {
|
|
|
7815
8632
|
amba_webhooks_deliveries_list: "infrastructure",
|
|
7816
8633
|
amba_webhooks_deliveries_get: "infrastructure",
|
|
7817
8634
|
amba_webhooks_deliveries_replay: "infrastructure",
|
|
8635
|
+
amba_events_catalog: "infrastructure",
|
|
8636
|
+
amba_control_webhooks_create: "infrastructure",
|
|
8637
|
+
amba_control_webhooks_list: "infrastructure",
|
|
8638
|
+
amba_control_webhooks_delete: "infrastructure",
|
|
7818
8639
|
amba_email_templates_create: "engagement",
|
|
7819
8640
|
amba_email_templates_list: "engagement",
|
|
7820
8641
|
amba_email_templates_get: "engagement",
|
|
@@ -7850,7 +8671,13 @@ function getToolCategory(toolName) {
|
|
|
7850
8671
|
* called against it once.
|
|
7851
8672
|
*/
|
|
7852
8673
|
function registerAllTools(server, apiClient) {
|
|
7853
|
-
registerTools$
|
|
8674
|
+
registerTools$14(server, apiClient);
|
|
8675
|
+
registerTools$35(server, apiClient);
|
|
8676
|
+
registerTools$34(server, apiClient);
|
|
8677
|
+
registerTools$33(server, apiClient);
|
|
8678
|
+
registerTools$32(server, apiClient);
|
|
8679
|
+
registerTools$31(server, apiClient);
|
|
8680
|
+
registerTools$30(server, apiClient);
|
|
7854
8681
|
registerTools$29(server, apiClient);
|
|
7855
8682
|
registerTools$28(server, apiClient);
|
|
7856
8683
|
registerTools$27(server, apiClient);
|
|
@@ -7866,10 +8693,10 @@ function registerAllTools(server, apiClient) {
|
|
|
7866
8693
|
registerTools$17(server, apiClient);
|
|
7867
8694
|
registerTools$16(server, apiClient);
|
|
7868
8695
|
registerTools$15(server, apiClient);
|
|
7869
|
-
registerTools$14(server, apiClient);
|
|
7870
8696
|
registerTools$13(server, apiClient);
|
|
7871
8697
|
registerTools$12(server, apiClient);
|
|
7872
8698
|
registerTools$11(server, apiClient);
|
|
8699
|
+
registerTools$10(server, apiClient);
|
|
7873
8700
|
registerTools$9(server, apiClient);
|
|
7874
8701
|
registerTools$8(server, apiClient);
|
|
7875
8702
|
registerTools$7(server, apiClient);
|