@layers/amba-mcp 4.0.5 → 4.0.6
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/auto/collection-tools.d.ts +63 -0
- package/dist/auto/describe.d.ts +39 -0
- package/dist/auto/index.d.ts +56 -0
- package/dist/auto/schema-to-zod.d.ts +87 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1496 -82
- package/dist/tools/monetization.d.ts +20 -0
- package/dist/tools/payments.d.ts +30 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -310,7 +310,13 @@ const READ_VERBS = new Set([
|
|
|
310
310
|
"logs",
|
|
311
311
|
"query",
|
|
312
312
|
"aggregate",
|
|
313
|
-
"results"
|
|
313
|
+
"results",
|
|
314
|
+
"explain",
|
|
315
|
+
"plan",
|
|
316
|
+
"diff",
|
|
317
|
+
"drift",
|
|
318
|
+
"balance",
|
|
319
|
+
"payouts"
|
|
314
320
|
]);
|
|
315
321
|
/**
|
|
316
322
|
* Destructive verbs. A tool whose name contains one of these MAY perform
|
|
@@ -367,11 +373,15 @@ const WRITE_VERBS = new Set([
|
|
|
367
373
|
"insert",
|
|
368
374
|
"invite",
|
|
369
375
|
"register",
|
|
376
|
+
"define",
|
|
377
|
+
"map",
|
|
378
|
+
"adopt",
|
|
370
379
|
"purchase",
|
|
371
380
|
"signup",
|
|
372
381
|
"login",
|
|
373
382
|
"refresh",
|
|
374
|
-
"track"
|
|
383
|
+
"track",
|
|
384
|
+
"charge"
|
|
375
385
|
]);
|
|
376
386
|
/**
|
|
377
387
|
* Split a tool name into lowercase verb-candidate tokens, dropping the
|
|
@@ -433,7 +443,9 @@ const NOUN_LIKE_VERBS = new Set([
|
|
|
433
443
|
"tiers",
|
|
434
444
|
"analytics",
|
|
435
445
|
"schedule",
|
|
436
|
-
"me"
|
|
446
|
+
"me",
|
|
447
|
+
"balance",
|
|
448
|
+
"payouts"
|
|
437
449
|
]);
|
|
438
450
|
/**
|
|
439
451
|
* The verbs eligible to be a title's leading word: every classification
|
|
@@ -624,7 +636,7 @@ function registerPublicTool(server, name, description, schema, handler, aliases
|
|
|
624
636
|
}
|
|
625
637
|
//#endregion
|
|
626
638
|
//#region src/tools/projects.ts
|
|
627
|
-
function registerTools$
|
|
639
|
+
function registerTools$37(server, apiClient) {
|
|
628
640
|
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 }) => {
|
|
629
641
|
const result = await client.get("/projects");
|
|
630
642
|
return { content: [{
|
|
@@ -731,7 +743,7 @@ function registerTools$35(server, apiClient) {
|
|
|
731
743
|
}
|
|
732
744
|
//#endregion
|
|
733
745
|
//#region src/tools/push.ts
|
|
734
|
-
function registerTools$
|
|
746
|
+
function registerTools$36(server, apiClient) {
|
|
735
747
|
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.", {
|
|
736
748
|
project_id: z.string().describe("The project ID"),
|
|
737
749
|
title: z.string().describe("Push notification title shown to the user"),
|
|
@@ -861,7 +873,7 @@ const segmentRulesSchema = z.object({
|
|
|
861
873
|
operator: z.enum(["AND", "OR"]).describe("Logical operator combining conditions"),
|
|
862
874
|
conditions: z.array(segmentConditionSchema).describe("Array of filter conditions")
|
|
863
875
|
});
|
|
864
|
-
function registerTools$
|
|
876
|
+
function registerTools$35(server, apiClient) {
|
|
865
877
|
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\").", {
|
|
866
878
|
project_id: z.string().describe("The project ID"),
|
|
867
879
|
include_system: z.boolean().optional().describe("Include built-in system segments in the result. Defaults to false.")
|
|
@@ -946,7 +958,7 @@ const configConditionSchema = z.object({
|
|
|
946
958
|
percentage: z.number().optional().describe("Percentage rollout (0-100)"),
|
|
947
959
|
value: z.unknown().describe("Override value for this condition")
|
|
948
960
|
});
|
|
949
|
-
function registerTools$
|
|
961
|
+
function registerTools$34(server, apiClient) {
|
|
950
962
|
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).", {
|
|
951
963
|
project_id: z.string().describe("The project ID"),
|
|
952
964
|
include_system: z.boolean().optional().describe("Include platform-provided default config keys in the result. Defaults to false.")
|
|
@@ -1024,7 +1036,7 @@ const contentItemSchema = z.object({
|
|
|
1024
1036
|
metadata: z.record(z.unknown()).optional().describe("Arbitrary metadata key-value pairs"),
|
|
1025
1037
|
is_premium: z.boolean().optional().describe("Whether this content requires a premium entitlement")
|
|
1026
1038
|
});
|
|
1027
|
-
function registerTools$
|
|
1039
|
+
function registerTools$33(server, apiClient) {
|
|
1028
1040
|
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`.", {
|
|
1029
1041
|
project_id: z.string().describe("The project ID"),
|
|
1030
1042
|
name: z.string().describe("Library name (e.g. \"Daily Motivation\", \"Workout Tips\")"),
|
|
@@ -1189,6 +1201,45 @@ function registerTools$31(server, apiClient) {
|
|
|
1189
1201
|
text: JSON.stringify(result, null, 2)
|
|
1190
1202
|
}] };
|
|
1191
1203
|
});
|
|
1204
|
+
registerTool(server, apiClient, "amba_content_translations_set", "Set a per-language translation for a content item. The base item carries one title/body in the language it was authored in; this stores a localized title/body for a BCP-47 language tag (e.g. \"es-419\", \"pt-BR\"). In-app, a content read in that language (the SDK forwards the device locale) returns the translated copy, falling back to the base item when no translation exists — so a multi-locale app ships every language from one Amba content library instead of bundling per-language copies in the app binary. This is a PARTIAL update: only the fields you pass are written. Omit a field to leave a previously-stored value untouched (and to share the base item value on first create). Pass an explicit `null` for `title` or `body` to CLEAR a previously-stored translated value so that field falls back to the base item again.", {
|
|
1205
|
+
project_id: z.string().describe("The project ID"),
|
|
1206
|
+
item_id: z.string().describe("The content item ID to translate"),
|
|
1207
|
+
language: z.string().describe("BCP-47 language tag for this translation (e.g. \"en\", \"es-419\", \"pt-BR\")"),
|
|
1208
|
+
title: z.string().nullable().optional().describe("Translated title. Omit to leave the existing value untouched (shares the base item title on first create); pass null to clear a previously-stored title so it falls back to the base item."),
|
|
1209
|
+
body: z.string().nullable().optional().describe("Translated body. Omit to leave the existing value untouched (shares the base item body on first create); pass null to clear a previously-stored body so it falls back to the base item."),
|
|
1210
|
+
metadata: z.record(z.unknown()).optional().describe("Optional per-language metadata (e.g. locale-specific media URL)")
|
|
1211
|
+
}, async ({ project_id, item_id, language, title, body, metadata }, { client }) => {
|
|
1212
|
+
const payload = {};
|
|
1213
|
+
if (title !== void 0) payload.title = title;
|
|
1214
|
+
if (body !== void 0) payload.body = body;
|
|
1215
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
1216
|
+
const result = await client.put(`/projects/${project_id}/content/items/${item_id}/translations/${encodeURIComponent(language)}`, payload);
|
|
1217
|
+
return { content: [{
|
|
1218
|
+
type: "text",
|
|
1219
|
+
text: JSON.stringify(result, null, 2)
|
|
1220
|
+
}] };
|
|
1221
|
+
});
|
|
1222
|
+
registerTool(server, apiClient, "amba_content_translations_list", "List every per-language translation for a content item (one row per language). Use this to see which locales an item is already translated into before adding more, or to audit translation coverage across a content library.", {
|
|
1223
|
+
project_id: z.string().describe("The project ID"),
|
|
1224
|
+
item_id: z.string().describe("The content item ID")
|
|
1225
|
+
}, async ({ project_id, item_id }, { client }) => {
|
|
1226
|
+
const result = await client.get(`/projects/${project_id}/content/items/${item_id}/translations`);
|
|
1227
|
+
return { content: [{
|
|
1228
|
+
type: "text",
|
|
1229
|
+
text: JSON.stringify(result, null, 2)
|
|
1230
|
+
}] };
|
|
1231
|
+
});
|
|
1232
|
+
registerTool(server, apiClient, "amba_content_translations_delete", "Delete a single per-language translation for a content item. After deletion, in-app reads in that language fall back to the base item.", {
|
|
1233
|
+
project_id: z.string().describe("The project ID"),
|
|
1234
|
+
item_id: z.string().describe("The content item ID"),
|
|
1235
|
+
language: z.string().describe("BCP-47 language tag of the translation to delete")
|
|
1236
|
+
}, async ({ project_id, item_id, language }, { client }) => {
|
|
1237
|
+
const result = await client.delete(`/projects/${project_id}/content/items/${item_id}/translations/${encodeURIComponent(language)}`);
|
|
1238
|
+
return { content: [{
|
|
1239
|
+
type: "text",
|
|
1240
|
+
text: JSON.stringify(result, null, 2)
|
|
1241
|
+
}] };
|
|
1242
|
+
});
|
|
1192
1243
|
registerTool(server, apiClient, "amba_content_bulk_import", "Bulk-import multiple content items into a library in a single transactional call. Each item is upsert-on-key within the library. Useful for seed data and for migrating from external content sources.", {
|
|
1193
1244
|
project_id: z.string().describe("The project ID"),
|
|
1194
1245
|
library_id: z.string().describe("The content library ID"),
|
|
@@ -1203,7 +1254,7 @@ function registerTools$31(server, apiClient) {
|
|
|
1203
1254
|
}
|
|
1204
1255
|
//#endregion
|
|
1205
1256
|
//#region src/tools/streaks.ts
|
|
1206
|
-
function registerTools$
|
|
1257
|
+
function registerTools$32(server, apiClient) {
|
|
1207
1258
|
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.", {
|
|
1208
1259
|
project_id: z.string().describe("The project ID"),
|
|
1209
1260
|
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`."),
|
|
@@ -1295,7 +1346,7 @@ function registerTools$30(server, apiClient) {
|
|
|
1295
1346
|
//#endregion
|
|
1296
1347
|
//#region src/tools/integrations.ts
|
|
1297
1348
|
const providerEnum = z.enum(INTEGRATION_PROVIDERS);
|
|
1298
|
-
function registerTools$
|
|
1349
|
+
function registerTools$31(server, apiClient) {
|
|
1299
1350
|
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.", {
|
|
1300
1351
|
project_id: z.string().describe("The project ID"),
|
|
1301
1352
|
provider: providerEnum.describe("Integration provider name"),
|
|
@@ -1365,7 +1416,7 @@ const PERIOD_DAYS = {
|
|
|
1365
1416
|
"30d": "30",
|
|
1366
1417
|
"90d": "90"
|
|
1367
1418
|
};
|
|
1368
|
-
function registerTools$
|
|
1419
|
+
function registerTools$30(server, apiClient) {
|
|
1369
1420
|
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.", {
|
|
1370
1421
|
project_id: z.string().describe("The project ID"),
|
|
1371
1422
|
period: z.enum([
|
|
@@ -1438,7 +1489,7 @@ function passthroughResult(result) {
|
|
|
1438
1489
|
}
|
|
1439
1490
|
//#endregion
|
|
1440
1491
|
//#region src/tools/users.ts
|
|
1441
|
-
function registerTools$
|
|
1492
|
+
function registerTools$29(server, apiClient) {
|
|
1442
1493
|
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.", {
|
|
1443
1494
|
project_id: z.string().describe("The project ID"),
|
|
1444
1495
|
limit: z.number().optional().describe("Maximum number of users to return (default 50)"),
|
|
@@ -1885,7 +1936,7 @@ final dailyLimit = await Amba.config.get<int>('daily_limit', defaultValue: 10);
|
|
|
1885
1936
|
final streak = await Amba.streaks.get('daily_login');
|
|
1886
1937
|
print('Current streak: \${streak.currentCount}');`
|
|
1887
1938
|
};
|
|
1888
|
-
function registerTools$
|
|
1939
|
+
function registerTools$28(server, apiClient) {
|
|
1889
1940
|
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([
|
|
1890
1941
|
"ios",
|
|
1891
1942
|
"android",
|
|
@@ -1970,7 +2021,7 @@ const criteriaSchema = z.object({
|
|
|
1970
2021
|
property_key: z.string().optional().describe("User property key (required for property_value type)"),
|
|
1971
2022
|
target_value: z.number().describe("Target value to unlock the achievement")
|
|
1972
2023
|
});
|
|
1973
|
-
function registerTools$
|
|
2024
|
+
function registerTools$27(server, apiClient) {
|
|
1974
2025
|
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.", {
|
|
1975
2026
|
project_id: z.string().describe("The project ID"),
|
|
1976
2027
|
key: z.string().describe("Unique key for this achievement (e.g. \"first_workout\", \"streak_master_7\")"),
|
|
@@ -2075,7 +2126,7 @@ function registerTools$25(server, apiClient) {
|
|
|
2075
2126
|
}
|
|
2076
2127
|
//#endregion
|
|
2077
2128
|
//#region src/tools/challenges.ts
|
|
2078
|
-
function registerTools$
|
|
2129
|
+
function registerTools$26(server, apiClient) {
|
|
2079
2130
|
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.", {
|
|
2080
2131
|
project_id: z.string().describe("The project ID"),
|
|
2081
2132
|
name: z.string().describe("Challenge name (e.g. \"7-Day Fitness Sprint\", \"XP Weekend Blitz\")"),
|
|
@@ -2186,7 +2237,7 @@ function registerTools$24(server, apiClient) {
|
|
|
2186
2237
|
}
|
|
2187
2238
|
//#endregion
|
|
2188
2239
|
//#region src/tools/economy.ts
|
|
2189
|
-
function registerTools$
|
|
2240
|
+
function registerTools$25(server, apiClient) {
|
|
2190
2241
|
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.", {
|
|
2191
2242
|
project_id: z.string().describe("The project ID"),
|
|
2192
2243
|
code: z.string().describe("Unique currency code (e.g. \"gold\", \"gems\", \"hearts\")"),
|
|
@@ -2443,12 +2494,12 @@ function registerTools$23(server, apiClient) {
|
|
|
2443
2494
|
app_user_id: z.string().describe("The app_user UUID to grant the entitlement to"),
|
|
2444
2495
|
entitlement_id: z.string().describe("Entitlement identifier (e.g. \"premium\", \"pro_annual\"). Required."),
|
|
2445
2496
|
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("
|
|
2497
|
+
product_id: z.string().nullable().optional().describe("Product identifier this grant is for (e.g. \"pro_yearly\"). When it matches a declared product (amba_products_create), the product's mapped entitlements are granted too. Pass null to clear."),
|
|
2447
2498
|
store: z.enum([
|
|
2448
2499
|
"app_store",
|
|
2449
2500
|
"play_store",
|
|
2450
|
-
"
|
|
2451
|
-
]).nullable().optional().describe("Originating store. Pass null to clear."),
|
|
2501
|
+
"web"
|
|
2502
|
+
]).nullable().optional().describe("Originating store (\"web\" covers any non-native checkout). Pass null to clear."),
|
|
2452
2503
|
purchase_date: z.string().nullable().optional().describe("ISO-8601 purchase timestamp. Pass null to clear."),
|
|
2453
2504
|
expiration_date: z.string().nullable().optional().describe("ISO-8601 expiration timestamp. Pass null to clear (e.g. for a lifetime grant)."),
|
|
2454
2505
|
period_type: z.enum([
|
|
@@ -2472,6 +2523,99 @@ function registerTools$23(server, apiClient) {
|
|
|
2472
2523
|
text: JSON.stringify(result, null, 2)
|
|
2473
2524
|
}] };
|
|
2474
2525
|
});
|
|
2526
|
+
registerTool(server, apiClient, "amba_entitlements_define", ["Define a canonical entitlement your app gates features on with `Amba.entitlements.has(\"pro\")`. Declaring it lets every grant path validate against a known set instead of free-typed strings, and lets you map products to it.", "This is the durable abstraction: features check the entitlement, never a product or a store — so you can change how it is sold without touching app code."].join(" "), {
|
|
2527
|
+
project_id: z.string().describe("The project ID"),
|
|
2528
|
+
key: z.string().describe("Stable entitlement key (e.g. \"pro\", \"premium_content\"). Required."),
|
|
2529
|
+
display_name: z.string().optional().describe("Human-readable name for dashboards."),
|
|
2530
|
+
description: z.string().optional().describe("What this entitlement unlocks."),
|
|
2531
|
+
metadata: z.record(z.unknown()).optional().describe("Opaque metadata object.")
|
|
2532
|
+
}, async ({ project_id, key, display_name, description, metadata }, { client }) => {
|
|
2533
|
+
const payload = { key };
|
|
2534
|
+
if (display_name !== void 0) payload.display_name = display_name;
|
|
2535
|
+
if (description !== void 0) payload.description = description;
|
|
2536
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
2537
|
+
const result = await client.post(`/projects/${project_id}/subscriptions/entitlements`, payload);
|
|
2538
|
+
return { content: [{
|
|
2539
|
+
type: "text",
|
|
2540
|
+
text: JSON.stringify(result, null, 2)
|
|
2541
|
+
}] };
|
|
2542
|
+
});
|
|
2543
|
+
registerTool(server, apiClient, "amba_products_create", ["Declare a sellable subscription product and the entitlement it grants. `store_product_refs` maps each store to its own identifier ({ \"app_store\": \"com.app.pro.yearly\", \"play_store\": \"pro_yearly\", \"web\": \"price_123\" }) so one Amba product resolves on whichever store a purchase came from.", "When `grants_entitlement_id` is set, an in-app purchase OR a server grant carrying this product unlocks that entitlement automatically — that resolution is what makes the payment engine swappable."].join(" "), {
|
|
2544
|
+
project_id: z.string().describe("The project ID"),
|
|
2545
|
+
product_id: z.string().describe("Stable, neutral product key (e.g. \"pro_yearly\"). Required."),
|
|
2546
|
+
grants_entitlement_id: z.string().optional().describe("The entitlement key this product unlocks (must be defined first)."),
|
|
2547
|
+
display_name: z.string().optional().describe("Human-readable product name."),
|
|
2548
|
+
description: z.string().optional().describe("Marketing description for the paywall."),
|
|
2549
|
+
product_type: z.enum([
|
|
2550
|
+
"subscription",
|
|
2551
|
+
"consumable",
|
|
2552
|
+
"non_consumable",
|
|
2553
|
+
"non_renewing"
|
|
2554
|
+
]).optional().describe("Product type (default \"subscription\")."),
|
|
2555
|
+
store_product_refs: z.record(z.string()).optional().describe("Map of store id → that store's product identifier."),
|
|
2556
|
+
trial_period_days: z.number().int().nonnegative().optional().describe("Free-trial length in days (display hint)."),
|
|
2557
|
+
duration: z.string().optional().describe("ISO-8601 duration display hint (e.g. \"P1M\", \"P1Y\")."),
|
|
2558
|
+
metadata: z.record(z.unknown()).optional().describe("Opaque metadata object.")
|
|
2559
|
+
}, async ({ project_id, product_id, grants_entitlement_id, display_name, description, product_type, store_product_refs, trial_period_days, duration, metadata }, { client }) => {
|
|
2560
|
+
const payload = { product_id };
|
|
2561
|
+
if (grants_entitlement_id !== void 0) payload.grants_entitlement_id = grants_entitlement_id;
|
|
2562
|
+
if (display_name !== void 0) payload.display_name = display_name;
|
|
2563
|
+
if (description !== void 0) payload.description = description;
|
|
2564
|
+
if (product_type !== void 0) payload.product_type = product_type;
|
|
2565
|
+
if (store_product_refs !== void 0) payload.store_product_refs = store_product_refs;
|
|
2566
|
+
if (trial_period_days !== void 0) payload.trial_period_days = trial_period_days;
|
|
2567
|
+
if (duration !== void 0) payload.duration = duration;
|
|
2568
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
2569
|
+
const result = await client.post(`/projects/${project_id}/subscriptions/products`, payload);
|
|
2570
|
+
return { content: [{
|
|
2571
|
+
type: "text",
|
|
2572
|
+
text: JSON.stringify(result, null, 2)
|
|
2573
|
+
}] };
|
|
2574
|
+
});
|
|
2575
|
+
registerTool(server, apiClient, "amba_entitlements_map_product", "Map a product to an (additional) entitlement it unlocks. Products can unlock more than one entitlement (e.g. a bundle); this adds to the set beyond the product's primary grants_entitlement_id. Both the product and the entitlement must already be declared.", {
|
|
2576
|
+
project_id: z.string().describe("The project ID"),
|
|
2577
|
+
product_id: z.string().describe("The declared product id."),
|
|
2578
|
+
entitlement_id: z.string().describe("The declared entitlement key to also grant.")
|
|
2579
|
+
}, async ({ project_id, product_id, entitlement_id }, { client }) => {
|
|
2580
|
+
const result = await client.post(`/projects/${project_id}/subscriptions/products/${product_id}/entitlements`, { entitlement_id });
|
|
2581
|
+
return { content: [{
|
|
2582
|
+
type: "text",
|
|
2583
|
+
text: JSON.stringify(result, null, 2)
|
|
2584
|
+
}] };
|
|
2585
|
+
});
|
|
2586
|
+
registerTool(server, apiClient, "amba_offerings_create", ["Create (or replace) a subscription offering — the named set of packages your paywall renders, fetched at runtime with `Amba.offerings()`. Each package surfaces one product at a position (e.g. $monthly, $annual).", "Set is_current=true to make this the default offering the SDK returns; exactly one offering is current at a time. The response is fully provider-neutral."].join(" "), {
|
|
2587
|
+
project_id: z.string().describe("The project ID"),
|
|
2588
|
+
offering_id: z.string().describe("Stable offering id (e.g. \"default\", \"holiday_2026\")."),
|
|
2589
|
+
display_name: z.string().optional().describe("Human-readable name."),
|
|
2590
|
+
description: z.string().optional().describe("Internal description."),
|
|
2591
|
+
is_current: z.boolean().optional().describe("Make this the default offering the SDK returns (default false)."),
|
|
2592
|
+
packages: z.array(z.object({
|
|
2593
|
+
package_id: z.string().describe("Package id unique within the offering (e.g. \"$annual\")."),
|
|
2594
|
+
product_id: z.string().optional().describe("The product this package surfaces (by product_id)."),
|
|
2595
|
+
position: z.number().int().nonnegative().optional().describe("Display order (lower first)."),
|
|
2596
|
+
metadata: z.record(z.unknown()).optional().describe("Opaque package metadata.")
|
|
2597
|
+
})).optional().describe("The packages in this offering (replaces the existing set)."),
|
|
2598
|
+
metadata: z.record(z.unknown()).optional().describe("Opaque offering metadata.")
|
|
2599
|
+
}, async ({ project_id, offering_id, display_name, description, is_current, packages, metadata }, { client }) => {
|
|
2600
|
+
const payload = { offering_id };
|
|
2601
|
+
if (display_name !== void 0) payload.display_name = display_name;
|
|
2602
|
+
if (description !== void 0) payload.description = description;
|
|
2603
|
+
if (is_current !== void 0) payload.is_current = is_current;
|
|
2604
|
+
if (packages !== void 0) payload.packages = packages;
|
|
2605
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
2606
|
+
const result = await client.post(`/projects/${project_id}/subscriptions/offerings`, payload);
|
|
2607
|
+
return { content: [{
|
|
2608
|
+
type: "text",
|
|
2609
|
+
text: JSON.stringify(result, null, 2)
|
|
2610
|
+
}] };
|
|
2611
|
+
});
|
|
2612
|
+
registerTool(server, apiClient, "amba_offerings_list", "List the project's subscription offerings with their packages and the product behind each — the same provider-neutral shape the client SDK's `offerings()` returns.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
2613
|
+
const result = await client.get(`/projects/${project_id}/subscriptions/offerings`);
|
|
2614
|
+
return { content: [{
|
|
2615
|
+
type: "text",
|
|
2616
|
+
text: JSON.stringify(result, null, 2)
|
|
2617
|
+
}] };
|
|
2618
|
+
});
|
|
2475
2619
|
registerTool(server, apiClient, "amba_users_get_inventory", "View a user's inventory including all owned items and quantities.", {
|
|
2476
2620
|
project_id: z.string().describe("The project ID"),
|
|
2477
2621
|
app_user_id: z.string().describe("The user ID to look up")
|
|
@@ -2776,8 +2920,81 @@ function registerTools$23(server, apiClient) {
|
|
|
2776
2920
|
});
|
|
2777
2921
|
}
|
|
2778
2922
|
//#endregion
|
|
2923
|
+
//#region src/tools/monetization.ts
|
|
2924
|
+
const objectTypeSchema = z.enum([
|
|
2925
|
+
"entitlement",
|
|
2926
|
+
"product",
|
|
2927
|
+
"offering",
|
|
2928
|
+
"package",
|
|
2929
|
+
"paywall"
|
|
2930
|
+
]);
|
|
2931
|
+
function registerTools$24(server, apiClient) {
|
|
2932
|
+
registerTool(server, apiClient, "amba_monetization_plan", "Preview the monetization plan for a project: a three-way diff between the declared subscription config (entitlements, products, offerings, packages, paywalls), the last-adopted baseline, and the live config in the connected provider (RevenueCat). Returns the op-list an apply WOULD perform (create/update/archive/attach/detach/set-current), a store-floor preflight (referenced store product identifiers not yet present upstream), and any out-of-band drift. READ-ONLY — computes the plan; applies nothing. Use this before changing monetization so you can see exactly what differs.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
2933
|
+
const result = await client.get(`/projects/${project_id}/monetization/plan`);
|
|
2934
|
+
return { content: [{
|
|
2935
|
+
type: "text",
|
|
2936
|
+
text: JSON.stringify(result, null, 2)
|
|
2937
|
+
}] };
|
|
2938
|
+
});
|
|
2939
|
+
registerTool(server, apiClient, "amba_monetization_drift", "Show only the out-of-band drift for a project's monetization: Amba-managed subscription objects whose live state in the provider (RevenueCat) has changed since Amba last adopted them. READ-ONLY — reports drift; reconciles nothing. Use this to detect when someone edited the subscription config directly in the provider dashboard outside the declarative flow.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
2940
|
+
const result = await client.get(`/projects/${project_id}/monetization/drift`);
|
|
2941
|
+
return { content: [{
|
|
2942
|
+
type: "text",
|
|
2943
|
+
text: JSON.stringify(result, null, 2)
|
|
2944
|
+
}] };
|
|
2945
|
+
});
|
|
2946
|
+
registerTool(server, apiClient, "amba_monetization_export", "Snapshot a project's LIVE subscription monetization config (from the connected provider, RevenueCat) into a declarative bundle: entitlements, products, offerings, packages, and paywalls keyed by their stable identifiers. READ-ONLY. Use this to capture the current config as Infrastructure-as-Code you can review, version-control, and promote between projects.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
2947
|
+
const result = await client.get(`/projects/${project_id}/monetization/export`);
|
|
2948
|
+
return { content: [{
|
|
2949
|
+
type: "text",
|
|
2950
|
+
text: JSON.stringify(result, null, 2)
|
|
2951
|
+
}] };
|
|
2952
|
+
});
|
|
2953
|
+
registerTool(server, apiClient, "amba_monetization_definitions_list", "List the project's current DECLARED monetization definitions (the desired-state entitlements, products, offerings, packages, and paywalls stored in Amba). READ-ONLY. This is the authored Infrastructure-as-Code that `amba_monetization_plan` diffs against the live provider config.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
2954
|
+
const result = await client.get(`/projects/${project_id}/monetization/definitions`);
|
|
2955
|
+
return { content: [{
|
|
2956
|
+
type: "text",
|
|
2957
|
+
text: JSON.stringify(result, null, 2)
|
|
2958
|
+
}] };
|
|
2959
|
+
});
|
|
2960
|
+
registerTool(server, apiClient, "amba_monetization_apply", "Apply the declared monetization config to the connected provider (RevenueCat). Additive ops apply automatically: CREATE entitlements/offerings/packages/paywall-DRAFTS, ATTACH products to new packages/entitlements, update display metadata. DESTRUCTIVE ops (detach a product, archive an entitlement/offering, remove a package) are GATED: they apply ONLY when you re-call with `confirm` set to the plan_hash — otherwise the apply refuses with MONETIZATION_CONFIRM_REQUIRED and lists the gated ops (nothing is written). Setting the current/default offering and publishing a paywall live are NOT API-driven — they're returned in `refused` with the exact dashboard step. Pass the `plan_hash` from a fresh `amba_monetization_plan`; apply refuses a stale plan_hash (re-plan), hard-fails on a missing store product, enforces the ordering invariant (never archives the live current offering; never detaches a product a live customer resolves unless `allow_detach_live`), reads back every write, and STOPS on the first failure without rolling back. To execute gated ops, first review the plan, then call again with confirm = plan_hash.", {
|
|
2961
|
+
project_id: z.string().describe("The project ID"),
|
|
2962
|
+
plan_hash: z.string().describe("The plan_hash returned by a fresh amba_monetization_plan. Apply refuses if the provider drifted off this hash."),
|
|
2963
|
+
confirm: z.string().optional().describe("Set to the SAME plan_hash to confirm and EXECUTE the gated destructive ops (detach/archive/remove). Omit for an additive-only apply — gated ops are then reported but not run."),
|
|
2964
|
+
allow_detach_live: z.boolean().optional().describe("Accept detaching a product a live customer currently resolves. Off by default (the apply refuses such a detach to protect live purchases)."),
|
|
2965
|
+
reconcile: z.enum(["amba", "adopt"]).optional().describe("Out-of-band drift mode: 'amba' (default) reverts the provider to the declared config on confirm; 'adopt' pulls the live change back into the declared config instead.")
|
|
2966
|
+
}, async ({ project_id, plan_hash, confirm, allow_detach_live, reconcile }, { client }) => {
|
|
2967
|
+
const body = { plan_hash };
|
|
2968
|
+
if (confirm !== void 0) body["confirm"] = confirm;
|
|
2969
|
+
if (allow_detach_live !== void 0) body["allow_detach_live"] = allow_detach_live;
|
|
2970
|
+
if (reconcile !== void 0) body["reconcile"] = reconcile;
|
|
2971
|
+
const result = await client.post(`/projects/${project_id}/monetization/apply`, body);
|
|
2972
|
+
return { content: [{
|
|
2973
|
+
type: "text",
|
|
2974
|
+
text: JSON.stringify(result, null, 2)
|
|
2975
|
+
}] };
|
|
2976
|
+
});
|
|
2977
|
+
registerTool(server, apiClient, "amba_monetization_adopt", "Adopt a project's live subscription config as Amba's managed baseline: writes the matching declared definitions AND records the current live state as the adopted baseline, so the NEXT plan is a clean no-op and future provider-side edits surface as drift. This writes ONLY Amba's own state — it NEVER changes the provider (RevenueCat). Idempotent. Pass adopt_all: true to adopt everything, or a specific objects list. Use this to bring an existing project's monetization under declarative management.", {
|
|
2978
|
+
project_id: z.string().describe("The project ID"),
|
|
2979
|
+
adopt_all: z.boolean().optional().describe("Adopt every live object. Mutually sufficient with `objects`."),
|
|
2980
|
+
objects: z.array(z.object({
|
|
2981
|
+
object_type: objectTypeSchema.describe("The kind of object to adopt"),
|
|
2982
|
+
identifier: z.string().describe("The object stable identifier (provider id)")
|
|
2983
|
+
})).optional().describe("Specific objects to adopt. Provide this OR adopt_all.")
|
|
2984
|
+
}, async ({ project_id, adopt_all, objects }, { client }) => {
|
|
2985
|
+
const body = {};
|
|
2986
|
+
if (adopt_all !== void 0) body["adopt_all"] = adopt_all;
|
|
2987
|
+
if (objects !== void 0) body["objects"] = objects;
|
|
2988
|
+
const result = await client.post(`/projects/${project_id}/monetization/adopt`, body);
|
|
2989
|
+
return { content: [{
|
|
2990
|
+
type: "text",
|
|
2991
|
+
text: JSON.stringify(result, null, 2)
|
|
2992
|
+
}] };
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2995
|
+
//#endregion
|
|
2779
2996
|
//#region src/tools/leaderboards.ts
|
|
2780
|
-
function registerTools$
|
|
2997
|
+
function registerTools$23(server, apiClient) {
|
|
2781
2998
|
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)`.", {
|
|
2782
2999
|
project_id: z.string().describe("The project ID"),
|
|
2783
3000
|
name: z.string().describe("Leaderboard name (e.g. \"Top XP Earners\", \"Weekly Streak Leaders\")"),
|
|
@@ -2892,7 +3109,7 @@ function registerTools$22(server, apiClient) {
|
|
|
2892
3109
|
* rollover (the scheduled weekly rollover) and score live off `xp_awarded`
|
|
2893
3110
|
* engagement events.
|
|
2894
3111
|
*/
|
|
2895
|
-
function registerTools$
|
|
3112
|
+
function registerTools$22(server, apiClient) {
|
|
2896
3113
|
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
3114
|
project_id: z.string().describe("The project ID"),
|
|
2898
3115
|
name: z.string().describe("League tier name, e.g. \"Bronze\", \"Silver\", \"Gold\""),
|
|
@@ -2955,7 +3172,7 @@ function registerTools$21(server, apiClient) {
|
|
|
2955
3172
|
}
|
|
2956
3173
|
//#endregion
|
|
2957
3174
|
//#region src/tools/platform.ts
|
|
2958
|
-
function registerTools$
|
|
3175
|
+
function registerTools$21(server, apiClient) {
|
|
2959
3176
|
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).", {
|
|
2960
3177
|
project_id: z.string().describe("The project ID"),
|
|
2961
3178
|
name: z.string().describe("Flow name (e.g. \"Welcome Flow\", \"Premium Onboarding\")"),
|
|
@@ -3230,6 +3447,83 @@ function registerTools$20(server, apiClient) {
|
|
|
3230
3447
|
text: JSON.stringify(result, null, 2)
|
|
3231
3448
|
}] };
|
|
3232
3449
|
});
|
|
3450
|
+
registerTool(server, apiClient, "amba_media_catalogs_create", "Create a curated media catalog — a named, slug-addressable bundle of media assets your app fetches in one call. Reach for this when an app needs a stable, ordered set of public images/assets (onboarding illustrations, a sticker pack, home-screen heroes) instead of hard-coding asset URLs client-side. Items are added separately with amba_media_catalogs_add_item. `is_public` (default true) gates whether end-user apps can read it via Amba.media.catalog(slug); set it false for drafts.", {
|
|
3451
|
+
project_id: z.string().describe("The project ID"),
|
|
3452
|
+
slug: z.string().describe("URL-safe handle your app reads the catalog by (lowercase, digits, hyphens; e.g. \"onboarding-art\"). Unique per project."),
|
|
3453
|
+
name: z.string().describe("Human-readable catalog name (e.g. \"Onboarding Illustrations\")"),
|
|
3454
|
+
description: z.string().optional().describe("Optional description of what the catalog holds"),
|
|
3455
|
+
is_public: z.boolean().optional().describe("Whether end-user apps can read this catalog by slug. Defaults to true."),
|
|
3456
|
+
metadata: z.record(z.unknown()).optional().describe("Custom metadata")
|
|
3457
|
+
}, async ({ project_id, slug, name, description, is_public, metadata }, { client }) => {
|
|
3458
|
+
const payload = {
|
|
3459
|
+
slug,
|
|
3460
|
+
name
|
|
3461
|
+
};
|
|
3462
|
+
if (description !== void 0) payload.description = description;
|
|
3463
|
+
if (is_public !== void 0) payload.is_public = is_public;
|
|
3464
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
3465
|
+
return jsonResult$1(await client.post(`/projects/${project_id}/media/catalogs`, payload));
|
|
3466
|
+
});
|
|
3467
|
+
registerTool(server, apiClient, "amba_media_catalogs_list", "List media catalogs for a project, each with its item count. Use this to see which curated asset bundles exist before adding to or reading one.", {
|
|
3468
|
+
project_id: z.string().describe("The project ID"),
|
|
3469
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
3470
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
3471
|
+
}, async ({ project_id, limit, offset }, { client }) => {
|
|
3472
|
+
const query = {};
|
|
3473
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
3474
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
3475
|
+
return jsonResult$1(await client.get(`/projects/${project_id}/media/catalogs`, query));
|
|
3476
|
+
});
|
|
3477
|
+
registerTool(server, apiClient, "amba_media_catalogs_get", "Get a single media catalog and its ordered items, each resolved to a stable public asset URL. Use this to inspect exactly what an app will receive when it reads the catalog by slug.", {
|
|
3478
|
+
project_id: z.string().describe("The project ID"),
|
|
3479
|
+
catalog_id: z.string().describe("The catalog ID")
|
|
3480
|
+
}, async ({ project_id, catalog_id }, { client }) => {
|
|
3481
|
+
return jsonResult$1(await client.get(`/projects/${project_id}/media/catalogs/${catalog_id}`));
|
|
3482
|
+
});
|
|
3483
|
+
registerTool(server, apiClient, "amba_media_catalogs_update", "Update a media catalog's name, slug, description, visibility, or metadata. Flip `is_public` to publish a draft catalog or pull one back to internal.", {
|
|
3484
|
+
project_id: z.string().describe("The project ID"),
|
|
3485
|
+
catalog_id: z.string().describe("The catalog ID"),
|
|
3486
|
+
slug: z.string().optional().describe("New URL-safe slug (lowercase, digits, hyphens)"),
|
|
3487
|
+
name: z.string().optional().describe("New catalog name"),
|
|
3488
|
+
description: z.string().optional().describe("New description"),
|
|
3489
|
+
is_public: z.boolean().optional().describe("Whether end-user apps can read this catalog"),
|
|
3490
|
+
metadata: z.record(z.unknown()).optional().describe("Replacement metadata object")
|
|
3491
|
+
}, async ({ project_id, catalog_id, slug, name, description, is_public, metadata }, { client }) => {
|
|
3492
|
+
const payload = {};
|
|
3493
|
+
if (slug !== void 0) payload.slug = slug;
|
|
3494
|
+
if (name !== void 0) payload.name = name;
|
|
3495
|
+
if (description !== void 0) payload.description = description;
|
|
3496
|
+
if (is_public !== void 0) payload.is_public = is_public;
|
|
3497
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
3498
|
+
return jsonResult$1(await client.patch(`/projects/${project_id}/media/catalogs/${catalog_id}`, payload));
|
|
3499
|
+
});
|
|
3500
|
+
registerTool(server, apiClient, "amba_media_catalogs_delete", "Delete a media catalog. This removes the catalog and its item membership; the underlying media assets are NOT deleted (they stay in storage and any other catalog).", {
|
|
3501
|
+
project_id: z.string().describe("The project ID"),
|
|
3502
|
+
catalog_id: z.string().describe("The catalog ID")
|
|
3503
|
+
}, async ({ project_id, catalog_id }, { client }) => {
|
|
3504
|
+
return jsonResult$1(await client.delete(`/projects/${project_id}/media/catalogs/${catalog_id}`));
|
|
3505
|
+
});
|
|
3506
|
+
registerTool(server, apiClient, "amba_media_catalogs_add_item", "Add a media asset to a catalog. The asset keeps its canonical public URL — a catalog is a curated reference, not a copy. Set `position` to control where it lands in the catalog's order, and `caption` for a per-catalog label.", {
|
|
3507
|
+
project_id: z.string().describe("The project ID"),
|
|
3508
|
+
catalog_id: z.string().describe("The catalog ID"),
|
|
3509
|
+
media_id: z.string().describe("ID of the media asset to add (from amba_media_list)"),
|
|
3510
|
+
position: z.number().optional().describe("Sort position within the catalog (lower comes first). Defaults to 0."),
|
|
3511
|
+
caption: z.string().optional().describe("Optional per-catalog caption for this asset"),
|
|
3512
|
+
metadata: z.record(z.unknown()).optional().describe("Custom metadata for this catalog item")
|
|
3513
|
+
}, async ({ project_id, catalog_id, media_id, position, caption, metadata }, { client }) => {
|
|
3514
|
+
const payload = { media_id };
|
|
3515
|
+
if (position !== void 0) payload.position = position;
|
|
3516
|
+
if (caption !== void 0) payload.caption = caption;
|
|
3517
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
3518
|
+
return jsonResult$1(await client.post(`/projects/${project_id}/media/catalogs/${catalog_id}/items`, payload));
|
|
3519
|
+
});
|
|
3520
|
+
registerTool(server, apiClient, "amba_media_catalogs_remove_item", "Remove a media asset from a catalog. The asset itself is untouched — only its membership in this catalog is removed.", {
|
|
3521
|
+
project_id: z.string().describe("The project ID"),
|
|
3522
|
+
catalog_id: z.string().describe("The catalog ID"),
|
|
3523
|
+
media_id: z.string().describe("ID of the media asset to remove from the catalog")
|
|
3524
|
+
}, async ({ project_id, catalog_id, media_id }, { client }) => {
|
|
3525
|
+
return jsonResult$1(await client.delete(`/projects/${project_id}/media/catalogs/${catalog_id}/items/${media_id}`));
|
|
3526
|
+
});
|
|
3233
3527
|
registerTool(server, apiClient, "amba_moderation_queue_get", "Get the content moderation queue for a project. Shows reported content pending review.", {
|
|
3234
3528
|
project_id: z.string().describe("The project ID"),
|
|
3235
3529
|
status: z.enum([
|
|
@@ -3531,7 +3825,7 @@ function registerTools$20(server, apiClient) {
|
|
|
3531
3825
|
}
|
|
3532
3826
|
//#endregion
|
|
3533
3827
|
//#region src/tools/social.ts
|
|
3534
|
-
function registerTools$
|
|
3828
|
+
function registerTools$20(server, apiClient) {
|
|
3535
3829
|
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 }) => {
|
|
3536
3830
|
const result = await client.get(`/projects/${project_id}/friends/stats`);
|
|
3537
3831
|
return { content: [{
|
|
@@ -3856,7 +4150,7 @@ function registerTools$19(server, apiClient) {
|
|
|
3856
4150
|
}
|
|
3857
4151
|
//#endregion
|
|
3858
4152
|
//#region src/tools/xp.ts
|
|
3859
|
-
function registerTools$
|
|
4153
|
+
function registerTools$19(server, apiClient) {
|
|
3860
4154
|
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.", {
|
|
3861
4155
|
project_id: z.string().describe("The project ID"),
|
|
3862
4156
|
name: z.string().describe("Rule name (e.g. \"Workout Completed\", \"Daily Login Bonus\")"),
|
|
@@ -3979,7 +4273,7 @@ function registerTools$18(server, apiClient) {
|
|
|
3979
4273
|
}
|
|
3980
4274
|
//#endregion
|
|
3981
4275
|
//#region src/tools/events.ts
|
|
3982
|
-
function registerTools$
|
|
4276
|
+
function registerTools$18(server, apiClient) {
|
|
3983
4277
|
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.", {
|
|
3984
4278
|
project_id: z.string().describe("The project ID"),
|
|
3985
4279
|
since: z.string().optional().describe("ISO-8601 lower bound for occurred_at. Defaults to 24h ago."),
|
|
@@ -4019,6 +4313,22 @@ function registerTools$17(server, apiClient) {
|
|
|
4019
4313
|
text: JSON.stringify(result, null, 2)
|
|
4020
4314
|
}] };
|
|
4021
4315
|
});
|
|
4316
|
+
registerTool(server, apiClient, "amba_events_explain", [
|
|
4317
|
+
"Dry-run an event against the rule engine: given a candidate event (name + properties + optional user), return which rules WOULD match and what each WOULD produce — XP awards, currency grants, feed items, streak qualification, and webhook deliveries — WITHOUT firing the event or committing any side effect. Pure read-only preview.",
|
|
4318
|
+
"Use it to answer \"what happens if I track this?\" before you wire up the call: see the exact XP delta, currency amount, feed item, and webhook destinations a given event would trigger, so you can confirm your rules are configured the way you intend.",
|
|
4319
|
+
"Pass `user_id` to also preview per-user gating (daily limits, cooldowns, balance caps) — the response notes when an effect would currently be suppressed or reduced. Omit it for the base, user-independent match. Nothing is written either way.",
|
|
4320
|
+
"Returns `{data: {matched: [{rule_type, rule_id, name, would, note?}], evaluated_surfaces, unmatched_reason_samples?}}`. An empty `matched` with a reason sample means no rule targets this event yet."
|
|
4321
|
+
].join(" "), {
|
|
4322
|
+
project_id: z.string().describe("The project ID"),
|
|
4323
|
+
event_name: z.string().describe("Candidate event name to evaluate (e.g. \"lesson_completed\")."),
|
|
4324
|
+
properties: z.record(z.unknown()).optional().describe("Candidate event properties (e.g. {amount: 5}). Read by property-scaled grant rules."),
|
|
4325
|
+
user_id: z.string().optional().describe("Optional app_user UUID. When supplied, the preview also reports per-user gating (daily limit / cooldown / balance cap) without mutating anything.")
|
|
4326
|
+
}, async ({ project_id, event_name, properties, user_id }, { client }) => {
|
|
4327
|
+
const event = { name: event_name };
|
|
4328
|
+
if (properties !== void 0) event.properties = properties;
|
|
4329
|
+
if (user_id !== void 0) event.user_id = user_id;
|
|
4330
|
+
return jsonResult$1(await client.post(`/projects/${project_id}/events/explain`, { event }));
|
|
4331
|
+
});
|
|
4022
4332
|
registerTool(server, apiClient, "amba_events_track", [
|
|
4023
4333
|
"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
4334
|
"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.",
|
|
@@ -4054,7 +4364,7 @@ function registerTools$17(server, apiClient) {
|
|
|
4054
4364
|
}
|
|
4055
4365
|
//#endregion
|
|
4056
4366
|
//#region src/tools/event-catalog.ts
|
|
4057
|
-
function registerTools$
|
|
4367
|
+
function registerTools$17(server, apiClient) {
|
|
4058
4368
|
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
4369
|
project_id: z.string().describe("The project ID"),
|
|
4060
4370
|
plane: z.enum(["app", "control"]).optional().describe("Filter to one plane. Omit for both.")
|
|
@@ -4124,7 +4434,7 @@ const funnelStepSchema = z.object({
|
|
|
4124
4434
|
event: z.string().describe("Event name to match (engagement event_name)."),
|
|
4125
4435
|
filters: z.array(funnelFilterSchema).optional().describe("Optional property predicates, ANDed together, that an event must satisfy.")
|
|
4126
4436
|
});
|
|
4127
|
-
function registerTools$
|
|
4437
|
+
function registerTools$16(server, apiClient) {
|
|
4128
4438
|
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 }) => {
|
|
4129
4439
|
const result = await client.get(`/projects/${project_id}/funnels`);
|
|
4130
4440
|
return { content: [{
|
|
@@ -4340,7 +4650,7 @@ function enrichedAuthResult(result, agentInstructions) {
|
|
|
4340
4650
|
status: result.status
|
|
4341
4651
|
});
|
|
4342
4652
|
}
|
|
4343
|
-
function registerTools$
|
|
4653
|
+
function registerTools$15(server, apiClient) {
|
|
4344
4654
|
registerPublicTool(server, "amba_developer_signup", [
|
|
4345
4655
|
"Create a new Amba developer account. Returns a long-lived Personal Access Token (PAT)",
|
|
4346
4656
|
"plus a real isolated Amba project (provisioning asynchronously), plus ready-to-paste",
|
|
@@ -4447,7 +4757,7 @@ function registerTools$14(server, apiClient) {
|
|
|
4447
4757
|
}
|
|
4448
4758
|
//#endregion
|
|
4449
4759
|
//#region src/tools/collections.ts
|
|
4450
|
-
async function adminFetch(apiClient, options) {
|
|
4760
|
+
async function adminFetch$1(apiClient, options) {
|
|
4451
4761
|
let url = `${apiClient.getBaseUrl()}${options.path}`;
|
|
4452
4762
|
if (options.query && Object.keys(options.query).length > 0) url += `?${new URLSearchParams(options.query).toString()}`;
|
|
4453
4763
|
const headers = {
|
|
@@ -4549,7 +4859,7 @@ const sdkOrderSchema = z.array(z.object({
|
|
|
4549
4859
|
})).describe("Ordering. `[{column: \"created_at\", direction: \"desc\"}, ...]`.");
|
|
4550
4860
|
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.");
|
|
4551
4861
|
const setSchema = z.record(z.unknown()).describe("Column-value map. Server-managed columns (id, created_at, etc.) are rejected.");
|
|
4552
|
-
function registerTools$
|
|
4862
|
+
function registerTools$14(server, apiClient) {
|
|
4553
4863
|
registerTool(server, apiClient, "amba_collections_create", [
|
|
4554
4864
|
"Create a new collection (schema-first Postgres table) in a project.",
|
|
4555
4865
|
"The schema change is applied server-side via an async workflow.",
|
|
@@ -4569,7 +4879,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4569
4879
|
};
|
|
4570
4880
|
if (indexes !== void 0) body.indexes = indexes;
|
|
4571
4881
|
if (shared !== void 0) body.shared = shared;
|
|
4572
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
4882
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4573
4883
|
method: "POST",
|
|
4574
4884
|
path: `/projects/${project_id}/collections`,
|
|
4575
4885
|
body,
|
|
@@ -4588,7 +4898,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4588
4898
|
const query = {};
|
|
4589
4899
|
if (limit !== void 0) query.limit = String(limit);
|
|
4590
4900
|
if (offset !== void 0) query.offset = String(offset);
|
|
4591
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
4901
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4592
4902
|
method: "GET",
|
|
4593
4903
|
path: `/projects/${project_id}/collections`,
|
|
4594
4904
|
query,
|
|
@@ -4603,7 +4913,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4603
4913
|
project_id: z.string().describe("The project ID."),
|
|
4604
4914
|
name: z.string().describe("Collection name (customer-facing, no `coll_` prefix).")
|
|
4605
4915
|
}, async ({ project_id, name }, { pat }) => {
|
|
4606
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
4916
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4607
4917
|
method: "GET",
|
|
4608
4918
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}`,
|
|
4609
4919
|
bearer: pat
|
|
@@ -4618,7 +4928,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4618
4928
|
name: z.string().describe("Collection name to drop."),
|
|
4619
4929
|
confirm: z.string().describe("Must equal the collection name. Accident guard for destructive drops.")
|
|
4620
4930
|
}, async ({ project_id, name, confirm }, { pat }) => {
|
|
4621
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
4931
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4622
4932
|
method: "DELETE",
|
|
4623
4933
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}`,
|
|
4624
4934
|
query: { confirm },
|
|
@@ -4637,7 +4947,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4637
4947
|
}, async ({ project_id, name, hard }, { pat }) => {
|
|
4638
4948
|
const query = {};
|
|
4639
4949
|
if (hard === false) query.hard = "false";
|
|
4640
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
4950
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4641
4951
|
method: "DELETE",
|
|
4642
4952
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/data`,
|
|
4643
4953
|
query,
|
|
@@ -4680,7 +4990,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4680
4990
|
if (relax_user_id !== void 0) body.relax_user_id = relax_user_id;
|
|
4681
4991
|
const query = {};
|
|
4682
4992
|
if (drop_column !== void 0 && confirm !== void 0) query.confirm = confirm;
|
|
4683
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
4993
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4684
4994
|
method: "PATCH",
|
|
4685
4995
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}`,
|
|
4686
4996
|
body,
|
|
@@ -4716,7 +5026,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4716
5026
|
...row,
|
|
4717
5027
|
as_system: true
|
|
4718
5028
|
} : row;
|
|
4719
|
-
const result = await adminFetch(apiClient, {
|
|
5029
|
+
const result = await adminFetch$1(apiClient, {
|
|
4720
5030
|
method: "POST",
|
|
4721
5031
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
4722
5032
|
body: requestBody,
|
|
@@ -4747,7 +5057,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4747
5057
|
}, async ({ project_id, name, rows, on_conflict, return_minimal }, { pat }) => {
|
|
4748
5058
|
const body = { rows };
|
|
4749
5059
|
if (on_conflict !== void 0) body.on_conflict = on_conflict;
|
|
4750
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5060
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4751
5061
|
method: "POST",
|
|
4752
5062
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows:batch`,
|
|
4753
5063
|
body,
|
|
@@ -4779,7 +5089,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4779
5089
|
if (cursor !== void 0) findQuery.cursor = cursor;
|
|
4780
5090
|
if (select !== void 0) findQuery.select = select;
|
|
4781
5091
|
if (include_deleted !== void 0) findQuery.includeDeleted = include_deleted;
|
|
4782
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5092
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4783
5093
|
method: "GET",
|
|
4784
5094
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
4785
5095
|
query: Object.keys(findQuery).length > 0 ? { query: JSON.stringify(findQuery) } : void 0,
|
|
@@ -4814,7 +5124,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4814
5124
|
if (group_by !== void 0) body.group_by = group_by;
|
|
4815
5125
|
if (where !== void 0) body.where = where;
|
|
4816
5126
|
if (include_deleted !== void 0) body.includeDeleted = include_deleted;
|
|
4817
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5127
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4818
5128
|
method: "POST",
|
|
4819
5129
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows/aggregate`,
|
|
4820
5130
|
body,
|
|
@@ -4824,6 +5134,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4824
5134
|
registerTool(server, apiClient, "amba_admin_update_row", [
|
|
4825
5135
|
"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
5136
|
"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.",
|
|
5137
|
+
"JSON object values targeting jsonb columns MERGE into the stored object (shallow, atomic merge-patch) instead of replacing it, so concurrent writers patching different keys both survive. Arrays/scalars/null replace the column value. Pass `objects: \"replace\"` to overwrite the whole stored object instead.",
|
|
4827
5138
|
"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
5139
|
"For updating many rows by a filter instead of one id, use amba_admin_bulk_update.",
|
|
4829
5140
|
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
@@ -4833,15 +5144,17 @@ function registerTools$13(server, apiClient) {
|
|
|
4833
5144
|
name: z.string().describe("Collection name."),
|
|
4834
5145
|
id: z.string().describe("Row UUID to update."),
|
|
4835
5146
|
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
|
-
|
|
4838
|
-
|
|
5147
|
+
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)."),
|
|
5148
|
+
objects: z.enum(["merge", "replace"]).optional().describe("How JSON object values apply to jsonb columns: \"merge\" (default — shallow merge-patch into the stored object) or \"replace\" (overwrite the whole stored object).")
|
|
5149
|
+
}, async ({ project_id, name, id, set, expected, objects }, { pat }) => {
|
|
5150
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4839
5151
|
method: "PATCH",
|
|
4840
5152
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows/${encodeURIComponent(id)}`,
|
|
4841
5153
|
body: expected !== void 0 ? {
|
|
4842
5154
|
set,
|
|
4843
5155
|
expected
|
|
4844
5156
|
} : { set },
|
|
5157
|
+
query: objects === "replace" ? { objects: "replace" } : void 0,
|
|
4845
5158
|
bearer: pat
|
|
4846
5159
|
}));
|
|
4847
5160
|
});
|
|
@@ -4858,7 +5171,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4858
5171
|
id: z.string().describe("Row UUID to delete."),
|
|
4859
5172
|
hard: z.boolean().optional().describe("false (default) soft-deletes (sets deleted_at so the row stays recoverable); true permanently removes the row (irreversible).")
|
|
4860
5173
|
}, async ({ project_id, name, id, hard }, { pat }) => {
|
|
4861
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5174
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4862
5175
|
method: "DELETE",
|
|
4863
5176
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows/${encodeURIComponent(id)}`,
|
|
4864
5177
|
query: hard === true ? { hard: "true" } : void 0,
|
|
@@ -4869,21 +5182,24 @@ function registerTools$13(server, apiClient) {
|
|
|
4869
5182
|
"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
5183
|
"`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
5184
|
"`set` is the column→value map applied to every matched row. Server-managed columns are rejected with 400 PROTECTED_COLUMN.",
|
|
5185
|
+
"JSON object values targeting jsonb columns MERGE into the stored object per row (shallow, atomic merge-patch) instead of replacing it. Arrays/scalars/null replace the column value. Pass `objects: \"replace\"` to overwrite the whole stored object instead.",
|
|
4872
5186
|
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4873
5187
|
"Returns `{data: {updated: <n>, ids: [...]}}`. Errors: 400 WHERE_REQUIRED / PROTECTED_COLUMN / INVALID_COLUMN, 404 COLLECTION_NOT_FOUND."
|
|
4874
5188
|
].join(" "), {
|
|
4875
5189
|
project_id: z.string().describe("The project ID."),
|
|
4876
5190
|
name: z.string().describe("Collection name."),
|
|
4877
5191
|
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
|
-
|
|
4880
|
-
|
|
5192
|
+
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."),
|
|
5193
|
+
objects: z.enum(["merge", "replace"]).optional().describe("How JSON object values apply to jsonb columns: \"merge\" (default — shallow merge-patch into the stored object) or \"replace\" (overwrite the whole stored object).")
|
|
5194
|
+
}, async ({ project_id, name, set, where, objects }, { pat }) => {
|
|
5195
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4881
5196
|
method: "PATCH",
|
|
4882
5197
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
4883
5198
|
body: {
|
|
4884
5199
|
set,
|
|
4885
5200
|
where
|
|
4886
5201
|
},
|
|
5202
|
+
query: objects === "replace" ? { objects: "replace" } : void 0,
|
|
4887
5203
|
bearer: pat
|
|
4888
5204
|
}));
|
|
4889
5205
|
});
|
|
@@ -4903,7 +5219,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4903
5219
|
}, async ({ project_id, name, where, confirm, hard }, { pat }) => {
|
|
4904
5220
|
const query = { confirm: String(confirm) };
|
|
4905
5221
|
if (hard === true) query.hard = "true";
|
|
4906
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5222
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4907
5223
|
method: "DELETE",
|
|
4908
5224
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
4909
5225
|
body: { where },
|
|
@@ -4994,6 +5310,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4994
5310
|
});
|
|
4995
5311
|
server.tool("amba_client_update_row", [
|
|
4996
5312
|
"Update a single row by id. Auto-RLS — only succeeds if the row belongs to the signed-in app_user.",
|
|
5313
|
+
"JSON object values targeting jsonb columns MERGE into the stored object (shallow, atomic merge-patch) instead of replacing it, so concurrent writers patching different keys both survive. Arrays/scalars/null replace the column value. Pass `objects: \"replace\"` to overwrite the whole stored object instead.",
|
|
4997
5314
|
"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.",
|
|
4998
5315
|
"For bulk updates with a where-clause, omit `id` and pass `where` + `set`.",
|
|
4999
5316
|
"Server-managed columns and user_id are rejected with 400 INVALID_COLUMN.",
|
|
@@ -5005,10 +5322,12 @@ function registerTools$13(server, apiClient) {
|
|
|
5005
5322
|
set: setSchema.describe("Column-value map to write. Required."),
|
|
5006
5323
|
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."),
|
|
5007
5324
|
limit: z.number().optional().describe("Bulk-update cap (default 1000, max 10000). Ignored when `id` is set."),
|
|
5325
|
+
objects: z.enum(["merge", "replace"]).optional().describe("How JSON object values apply to jsonb columns: \"merge\" (default — shallow merge-patch into the stored object) or \"replace\" (overwrite the whole stored object)."),
|
|
5008
5326
|
api_key: z.string().describe("Project client X-Api-Key."),
|
|
5009
5327
|
session_token: z.string().optional().describe("App_user session Bearer. Required by the route.")
|
|
5010
|
-
}, async ({ name, id, where, set, expected, limit, api_key, session_token }) => {
|
|
5328
|
+
}, async ({ name, id, where, set, expected, limit, objects, api_key, session_token }) => {
|
|
5011
5329
|
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
5330
|
+
const objectsQuery = objects === "replace" ? { objects: "replace" } : void 0;
|
|
5012
5331
|
if (id !== void 0) return passthroughResult(await clientFetch(apiClient, {
|
|
5013
5332
|
method: "PATCH",
|
|
5014
5333
|
path: `/collections/${encodeURIComponent(name)}/${encodeURIComponent(id)}`,
|
|
@@ -5016,6 +5335,7 @@ function registerTools$13(server, apiClient) {
|
|
|
5016
5335
|
set,
|
|
5017
5336
|
expected
|
|
5018
5337
|
} : { set },
|
|
5338
|
+
...objectsQuery !== void 0 && { query: objectsQuery },
|
|
5019
5339
|
apiKey: api_key,
|
|
5020
5340
|
...session_token !== void 0 && { sessionToken: session_token }
|
|
5021
5341
|
}));
|
|
@@ -5028,6 +5348,7 @@ function registerTools$13(server, apiClient) {
|
|
|
5028
5348
|
method: "PATCH",
|
|
5029
5349
|
path: `/collections/${encodeURIComponent(name)}`,
|
|
5030
5350
|
body,
|
|
5351
|
+
...objectsQuery !== void 0 && { query: objectsQuery },
|
|
5031
5352
|
apiKey: api_key,
|
|
5032
5353
|
...session_token !== void 0 && { sessionToken: session_token }
|
|
5033
5354
|
}));
|
|
@@ -5324,7 +5645,7 @@ function jsonResult(payload) {
|
|
|
5324
5645
|
* of a generic upstream 400.
|
|
5325
5646
|
*/
|
|
5326
5647
|
const MAX_BUNDLE_BYTES = 10 * 1024 * 1024;
|
|
5327
|
-
function registerTools$
|
|
5648
|
+
function registerTools$13(server, apiClient) {
|
|
5328
5649
|
registerTool(server, apiClient, "amba_functions_deploy", [
|
|
5329
5650
|
"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`.",
|
|
5330
5651
|
"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).",
|
|
@@ -5452,7 +5773,7 @@ function registerTools$12(server, apiClient) {
|
|
|
5452
5773
|
}
|
|
5453
5774
|
//#endregion
|
|
5454
5775
|
//#region src/tools/sites.ts
|
|
5455
|
-
function registerTools$
|
|
5776
|
+
function registerTools$12(server, apiClient) {
|
|
5456
5777
|
registerTool(server, apiClient, "amba_sites_deploy", [
|
|
5457
5778
|
"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.",
|
|
5458
5779
|
"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`.",
|
|
@@ -5544,7 +5865,7 @@ function registerTools$11(server, apiClient) {
|
|
|
5544
5865
|
}
|
|
5545
5866
|
//#endregion
|
|
5546
5867
|
//#region src/tools/domains.ts
|
|
5547
|
-
function registerTools$
|
|
5868
|
+
function registerTools$11(server, apiClient) {
|
|
5548
5869
|
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(" "), {
|
|
5549
5870
|
project_id: z.string().describe("The Amba project ID."),
|
|
5550
5871
|
query: z.string().describe("A keyword (e.g. \"getunbury\") or a full domain (e.g. \"unbury.com\")."),
|
|
@@ -5600,7 +5921,7 @@ function registerTools$10(server, apiClient) {
|
|
|
5600
5921
|
}
|
|
5601
5922
|
//#endregion
|
|
5602
5923
|
//#region src/tools/operations.ts
|
|
5603
|
-
function registerTools$
|
|
5924
|
+
function registerTools$10(server, apiClient) {
|
|
5604
5925
|
registerTool(server, apiClient, "amba_operations_get", [
|
|
5605
5926
|
"Poll an async operation by its operation_id to see whether it has finished.",
|
|
5606
5927
|
"Returns the operation `status` (pending | running | succeeded | failed); on `failed`, `failed_reason` says why, and on `succeeded`, `result` holds the outcome.",
|
|
@@ -5633,7 +5954,7 @@ function registerTools$9(server, apiClient) {
|
|
|
5633
5954
|
}
|
|
5634
5955
|
//#endregion
|
|
5635
5956
|
//#region src/tools/secrets.ts
|
|
5636
|
-
function registerTools$
|
|
5957
|
+
function registerTools$9(server, apiClient) {
|
|
5637
5958
|
registerTool(server, apiClient, "amba_secrets_set", [
|
|
5638
5959
|
"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.",
|
|
5639
5960
|
"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).",
|
|
@@ -5706,17 +6027,249 @@ function registerTools$8(server, apiClient) {
|
|
|
5706
6027
|
const PROVIDER_ENUM$1 = z.enum([
|
|
5707
6028
|
"anthropic",
|
|
5708
6029
|
"openai",
|
|
5709
|
-
"mistral"
|
|
6030
|
+
"mistral",
|
|
6031
|
+
"gemini"
|
|
5710
6032
|
]);
|
|
5711
6033
|
const RATE_LIMIT_SHAPE = z.object({
|
|
5712
6034
|
window: z.string().optional().describe("Window length (e.g. \"60s\", \"1m\", \"1h\"). Defaults to \"60s\"."),
|
|
5713
6035
|
max: z.number().int().min(1).optional().describe("Max invocations per window. Defaults to 20."),
|
|
5714
6036
|
key: z.string().optional().describe("Bucketing key. Typically \"user_id\" or \"session_id\". Defaults to \"user_id\".")
|
|
5715
6037
|
}).strict();
|
|
5716
|
-
|
|
6038
|
+
const DEFAULT_IMAGE_MIME = "image/png";
|
|
6039
|
+
/**
|
|
6040
|
+
* Normalize an image's `url` / `data` to non-empty values, trimming whitespace
|
|
6041
|
+
* and treating a blank result as absent. Returns `null` when NEITHER a real url
|
|
6042
|
+
* nor real data survives — such a block carries no image and is dropped by the
|
|
6043
|
+
* caller (instead of emitting an empty image URL or an empty base64 block).
|
|
6044
|
+
*/
|
|
6045
|
+
function normalizeImage(img) {
|
|
6046
|
+
const url = typeof img.url === "string" && img.url.trim().length > 0 ? img.url.trim() : void 0;
|
|
6047
|
+
const data = typeof img.data === "string" && img.data.trim().length > 0 ? img.data.trim() : void 0;
|
|
6048
|
+
if (!url && !data) return null;
|
|
6049
|
+
return {
|
|
6050
|
+
url,
|
|
6051
|
+
data,
|
|
6052
|
+
mime: img.mime
|
|
6053
|
+
};
|
|
6054
|
+
}
|
|
6055
|
+
/**
|
|
6056
|
+
* Map a NORMALIZED vendor-neutral image into the provider's native
|
|
6057
|
+
* content-block shape. Anthropic → `{type:'image', source:{...}}`;
|
|
6058
|
+
* OpenAI/Mistral → `{type:'image_url', image_url:{url}}` (base64 becomes a
|
|
6059
|
+
* data: URL); Gemini → a `parts[]` entry (`inline_data` for base64,
|
|
6060
|
+
* `file_data` for a URL). The input is guaranteed to have a real `url` or
|
|
6061
|
+
* real `data`.
|
|
6062
|
+
*
|
|
6063
|
+
* Note: Gemini's image shape is a `contents[].parts[]` entry, NOT a message
|
|
6064
|
+
* `content[]` block — so the Gemini path builds `contents[]` directly via
|
|
6065
|
+
* `geminiContentsFromMessages` and does NOT route image mapping through
|
|
6066
|
+
* `attachImagesToMessages` (which speaks the OpenAI/Anthropic message-content
|
|
6067
|
+
* shape). This helper is reused there for the per-part mapping.
|
|
6068
|
+
*/
|
|
6069
|
+
function imageBlockForProvider(img, provider) {
|
|
6070
|
+
const mime = img.mime ?? DEFAULT_IMAGE_MIME;
|
|
6071
|
+
if (provider === "anthropic") return img.url ? {
|
|
6072
|
+
type: "image",
|
|
6073
|
+
source: {
|
|
6074
|
+
type: "url",
|
|
6075
|
+
url: img.url
|
|
6076
|
+
}
|
|
6077
|
+
} : {
|
|
6078
|
+
type: "image",
|
|
6079
|
+
source: {
|
|
6080
|
+
type: "base64",
|
|
6081
|
+
media_type: mime,
|
|
6082
|
+
data: img.data ?? ""
|
|
6083
|
+
}
|
|
6084
|
+
};
|
|
6085
|
+
if (provider === "gemini") return img.url ? { file_data: {
|
|
6086
|
+
mime_type: mime,
|
|
6087
|
+
file_uri: img.url
|
|
6088
|
+
} } : { inline_data: {
|
|
6089
|
+
mime_type: mime,
|
|
6090
|
+
data: img.data ?? ""
|
|
6091
|
+
} };
|
|
6092
|
+
return {
|
|
6093
|
+
type: "image_url",
|
|
6094
|
+
image_url: { url: img.url ?? `data:${mime};base64,${img.data ?? ""}` }
|
|
6095
|
+
};
|
|
6096
|
+
}
|
|
6097
|
+
/**
|
|
6098
|
+
* Convert ONE content block from a message's content array into the provider's
|
|
6099
|
+
* native shape. A vendor-neutral `{type:'image', url|data}` block is mapped via
|
|
6100
|
+
* `imageBlockForProvider`; everything else (a `{type:'text'}` block, or an
|
|
6101
|
+
* already-native block the agent supplied) passes through unchanged. This keeps
|
|
6102
|
+
* the whole message in a SINGLE format so we never mix vendor-neutral and
|
|
6103
|
+
* pre-mapped native image blocks in one message (which would be malformed for
|
|
6104
|
+
* the raw `/ai/messages` passthrough).
|
|
6105
|
+
*
|
|
6106
|
+
* Returns `null` only for a vendor-neutral image block that normalizes away
|
|
6107
|
+
* (blank url + blank data) — the caller drops it.
|
|
6108
|
+
*/
|
|
6109
|
+
function nativeContentBlock(block, provider) {
|
|
6110
|
+
if (typeof block === "object" && block !== null) {
|
|
6111
|
+
const b = block;
|
|
6112
|
+
if (b["type"] === "image" && ("url" in b || "data" in b) && !("source" in b)) {
|
|
6113
|
+
const norm = normalizeImage({
|
|
6114
|
+
url: typeof b["url"] === "string" ? b["url"] : void 0,
|
|
6115
|
+
data: typeof b["data"] === "string" ? b["data"] : void 0,
|
|
6116
|
+
mime: typeof b["mime"] === "string" ? b["mime"] : void 0
|
|
6117
|
+
});
|
|
6118
|
+
return norm ? imageBlockForProvider(norm, provider) : null;
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6121
|
+
return block;
|
|
6122
|
+
}
|
|
6123
|
+
/** Does a message's `content` array carry any vendor-neutral image block? */
|
|
6124
|
+
function contentHasNeutralImage(content) {
|
|
6125
|
+
if (!Array.isArray(content)) return false;
|
|
6126
|
+
return content.some((b) => {
|
|
6127
|
+
if (typeof b !== "object" || b === null) return false;
|
|
6128
|
+
const bb = b;
|
|
6129
|
+
return bb["type"] === "image" && ("url" in bb || "data" in bb) && !("source" in bb);
|
|
6130
|
+
});
|
|
6131
|
+
}
|
|
6132
|
+
/** Does ANY message carry a vendor-neutral image block in its content array? */
|
|
6133
|
+
function messagesHaveNeutralImage(messages) {
|
|
6134
|
+
return messages.some((m) => contentHasNeutralImage(m?.content));
|
|
6135
|
+
}
|
|
6136
|
+
/**
|
|
6137
|
+
* Normalize ONE message's content array into a single native-shaped block list:
|
|
6138
|
+
* a string → a `[{type:'text', text}]` block (empty string → no block); an
|
|
6139
|
+
* array → its blocks mapped via `nativeContentBlock` (vendor-neutral images to
|
|
6140
|
+
* native, others untouched, normalize-away blocks dropped). Returns the block
|
|
6141
|
+
* list for the array/string cases; for any other value returns `null` so the
|
|
6142
|
+
* caller can leave it untouched (don't coerce a shape we don't understand).
|
|
6143
|
+
*/
|
|
6144
|
+
function normalizeContentBlocks(content, provider) {
|
|
6145
|
+
if (typeof content === "string") return content.length > 0 ? [{
|
|
6146
|
+
type: "text",
|
|
6147
|
+
text: content
|
|
6148
|
+
}] : [];
|
|
6149
|
+
if (Array.isArray(content)) return content.map((b) => nativeContentBlock(b, provider)).filter((b) => b !== null);
|
|
6150
|
+
return null;
|
|
6151
|
+
}
|
|
6152
|
+
/**
|
|
6153
|
+
* Map a registered prompt's `messages` into a single provider-native format AND
|
|
6154
|
+
* (optionally) attach extra `images` to the last user message.
|
|
6155
|
+
*
|
|
6156
|
+
* Because `amba_ai_prompts_invoke` posts through the raw `/ai/messages`
|
|
6157
|
+
* passthrough (byte-for-byte to the provider — the gateway does NOT remap this
|
|
6158
|
+
* path), any VENDOR-NEUTRAL `{type:'image', url|data}` block — whether it came
|
|
6159
|
+
* from the `images` convenience param OR was put directly in `messages` by the
|
|
6160
|
+
* caller — must be mapped to the provider's native shape HERE.
|
|
6161
|
+
*
|
|
6162
|
+
* Conservative by design: a message is rewritten ONLY when it needs it — it
|
|
6163
|
+
* carries vendor-neutral image blocks, OR it's the last user message receiving
|
|
6164
|
+
* appended `images`. Plain text / already-native messages pass through
|
|
6165
|
+
* byte-identical (no needless string→block conversion). When no user message
|
|
6166
|
+
* exists but extra `images` were supplied, a fresh user message is appended.
|
|
6167
|
+
* Returns a NEW array — the caller's `messages` is never mutated.
|
|
6168
|
+
*/
|
|
6169
|
+
function attachImagesToMessages(messages, images, provider) {
|
|
6170
|
+
const imageBlocks = images.map(normalizeImage).filter((img) => img !== null).map((img) => imageBlockForProvider(img, provider));
|
|
6171
|
+
const out = messages.map((m) => ({ ...m }));
|
|
6172
|
+
let lastUserIdx = -1;
|
|
6173
|
+
for (let i = out.length - 1; i >= 0; i--) if (out[i].role === "user") {
|
|
6174
|
+
lastUserIdx = i;
|
|
6175
|
+
break;
|
|
6176
|
+
}
|
|
6177
|
+
for (let i = 0; i < out.length; i++) {
|
|
6178
|
+
const isTarget = i === lastUserIdx && imageBlocks.length > 0;
|
|
6179
|
+
const needsImageMapping = contentHasNeutralImage(out[i].content);
|
|
6180
|
+
if (!isTarget && !needsImageMapping) continue;
|
|
6181
|
+
const blocks = normalizeContentBlocks(out[i].content, provider);
|
|
6182
|
+
if (blocks === null) {
|
|
6183
|
+
if (isTarget) out[i] = {
|
|
6184
|
+
...out[i],
|
|
6185
|
+
content: [{
|
|
6186
|
+
type: "text",
|
|
6187
|
+
text: JSON.stringify(out[i].content ?? "")
|
|
6188
|
+
}, ...imageBlocks]
|
|
6189
|
+
};
|
|
6190
|
+
continue;
|
|
6191
|
+
}
|
|
6192
|
+
const withImages = isTarget ? [...blocks, ...imageBlocks] : blocks;
|
|
6193
|
+
if (withImages.length === 0) continue;
|
|
6194
|
+
out[i] = {
|
|
6195
|
+
...out[i],
|
|
6196
|
+
content: withImages
|
|
6197
|
+
};
|
|
6198
|
+
}
|
|
6199
|
+
if (lastUserIdx === -1 && imageBlocks.length > 0) out.push({
|
|
6200
|
+
role: "user",
|
|
6201
|
+
content: imageBlocks
|
|
6202
|
+
});
|
|
6203
|
+
return out;
|
|
6204
|
+
}
|
|
6205
|
+
/**
|
|
6206
|
+
* Map ONE message's content into Gemini `parts`. A string → a single
|
|
6207
|
+
* `{text}` part; a non-string, non-array value → one JSON-stringified `{text}`
|
|
6208
|
+
* part; an array → each block mapped (vendor-neutral text → `{text}`,
|
|
6209
|
+
* vendor-neutral image → `inline_data`/`file_data`, a normalize-away image
|
|
6210
|
+
* dropped, and any UNKNOWN/alien block — e.g. an OpenAI `image_url` or
|
|
6211
|
+
* Anthropic `source` shape — dropped). Mirrors the gateway's
|
|
6212
|
+
* `mapContentGeminiParts` exactly so a server-side test invocation produces the
|
|
6213
|
+
* same upstream body the production `/prompts/:name/invoke` path does.
|
|
6214
|
+
*/
|
|
6215
|
+
function geminiPartsForContent(content) {
|
|
6216
|
+
if (typeof content === "string") return [{ text: content }];
|
|
6217
|
+
if (!Array.isArray(content)) return [{ text: JSON.stringify(content ?? "") }];
|
|
6218
|
+
const parts = [];
|
|
6219
|
+
for (const block of content) {
|
|
6220
|
+
if (typeof block !== "object" || block === null) continue;
|
|
6221
|
+
const b = block;
|
|
6222
|
+
if (b["type"] === "text" && typeof b["text"] === "string") {
|
|
6223
|
+
parts.push({ text: b["text"] });
|
|
6224
|
+
continue;
|
|
6225
|
+
}
|
|
6226
|
+
if (b["type"] === "image" && ("url" in b || "data" in b)) {
|
|
6227
|
+
const norm = normalizeImage({
|
|
6228
|
+
url: typeof b["url"] === "string" ? b["url"] : void 0,
|
|
6229
|
+
data: typeof b["data"] === "string" ? b["data"] : void 0,
|
|
6230
|
+
mime: typeof b["mime"] === "string" ? b["mime"] : void 0
|
|
6231
|
+
});
|
|
6232
|
+
if (norm) parts.push(imageBlockForProvider(norm, "gemini"));
|
|
6233
|
+
continue;
|
|
6234
|
+
}
|
|
6235
|
+
}
|
|
6236
|
+
return parts;
|
|
6237
|
+
}
|
|
6238
|
+
/**
|
|
6239
|
+
* Build Gemini's `contents[]` from a registered prompt's `messages`, mapping
|
|
6240
|
+
* roles (assistant/model → "model", everything else → "user"), translating
|
|
6241
|
+
* content into `parts`, and appending any extra `images` to the last user turn
|
|
6242
|
+
* (creating one if none exists). Messages whose content maps to NO parts are
|
|
6243
|
+
* dropped (an empty turn is rejected upstream). Returns a NEW array — the
|
|
6244
|
+
* caller's `messages` is never mutated.
|
|
6245
|
+
*/
|
|
6246
|
+
function geminiContentsFromMessages(messages, images) {
|
|
6247
|
+
const imageParts = images.map(normalizeImage).filter((img) => img !== null).map((img) => imageBlockForProvider(img, "gemini"));
|
|
6248
|
+
const contents = messages.map((m) => ({
|
|
6249
|
+
role: m.role === "assistant" || m.role === "model" ? "model" : "user",
|
|
6250
|
+
parts: geminiPartsForContent(m.content)
|
|
6251
|
+
}));
|
|
6252
|
+
if (imageParts.length > 0) {
|
|
6253
|
+
let lastUserIdx = -1;
|
|
6254
|
+
for (let i = contents.length - 1; i >= 0; i--) if (contents[i].role === "user") {
|
|
6255
|
+
lastUserIdx = i;
|
|
6256
|
+
break;
|
|
6257
|
+
}
|
|
6258
|
+
if (lastUserIdx === -1) contents.push({
|
|
6259
|
+
role: "user",
|
|
6260
|
+
parts: [...imageParts]
|
|
6261
|
+
});
|
|
6262
|
+
else contents[lastUserIdx] = {
|
|
6263
|
+
role: "user",
|
|
6264
|
+
parts: [...contents[lastUserIdx].parts, ...imageParts]
|
|
6265
|
+
};
|
|
6266
|
+
}
|
|
6267
|
+
return contents.filter((c) => c.parts.length > 0);
|
|
6268
|
+
}
|
|
6269
|
+
function registerTools$8(server, apiClient) {
|
|
5717
6270
|
registerTool(server, apiClient, "amba_ai_prompts_create", [
|
|
5718
6271
|
"Register a new AI prompt template on a project. Returns the persisted prompt row (name, version, provider, model, client_invokable).",
|
|
5719
|
-
"Name must match /^[a-z][a-z0-9_-]{0,127}$/. Provider must be \"anthropic\", \"openai\", or \"
|
|
6272
|
+
"Name must match /^[a-z][a-z0-9_-]{0,127}$/. Provider must be \"anthropic\", \"openai\", \"mistral\", or \"gemini\" 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\", \"gemini-2.5-flash\").",
|
|
5720
6273
|
"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.",
|
|
5721
6274
|
"Re-issuing this tool with an existing `name` will bump the prompt to a new version; prefer `amba_ai_prompts_update` for that intent."
|
|
5722
6275
|
].join(" "), {
|
|
@@ -5780,8 +6333,9 @@ function registerTools$7(server, apiClient) {
|
|
|
5780
6333
|
});
|
|
5781
6334
|
}, ["amba_delete_ai_prompt"]);
|
|
5782
6335
|
registerTool(server, apiClient, "amba_ai_prompts_invoke", [
|
|
5783
|
-
"Server-side test invocation of a registered AI prompt. Looks up the prompt's provider + model + system_prompt + max_tokens, then proxies
|
|
5784
|
-
"Provide `messages` as a
|
|
6336
|
+
"Server-side test invocation of a registered AI prompt. Looks up the prompt's provider + model + system_prompt + max_tokens, then proxies the call through the admin AI gateway using the customer's registered provider key. Works for every provider — Anthropic / OpenAI / Mistral build a Messages-style body; Gemini builds its contents[] body automatically.",
|
|
6337
|
+
"Provide `messages` as a simple `[{role: \"user\", content: \"...\"}]` array (roles: \"user\"/\"assistant\"/\"system\"; for Gemini, \"assistant\" maps to \"model\" and \"system\" is taken from the prompt). For testing — production traffic goes through the SDK or the function-side `ctx.ai.run()` helper.",
|
|
6338
|
+
"To send IMAGES to a vision-capable model, pass `images` (each `{url}` or `{data, mime}`) — they attach to the last user message, mapped to the provider's native image format for you. The prompt's model must be vision-capable.",
|
|
5785
6339
|
"`extra_body` lets you pass additional provider-native parameters (temperature, top_p, response_format, …). These are merged shallow on top of the prompt-derived body — so a `model` override here wins."
|
|
5786
6340
|
].join(" "), {
|
|
5787
6341
|
project_id: z.string().describe("The Amba project ID."),
|
|
@@ -5790,21 +6344,39 @@ function registerTools$7(server, apiClient) {
|
|
|
5790
6344
|
role: z.string().describe("Anthropic / OpenAI role (e.g. \"user\", \"assistant\", \"system\")."),
|
|
5791
6345
|
content: z.unknown().describe("Message content. String or provider-native array form.")
|
|
5792
6346
|
})).min(1).describe("Provider-shaped messages array. At least one message required."),
|
|
6347
|
+
images: z.array(z.object({
|
|
6348
|
+
url: z.string().optional().describe("Remote image URL. Use this OR `data` (base64), not both."),
|
|
6349
|
+
data: z.string().optional().describe("Base64-encoded image bytes (no data: prefix)."),
|
|
6350
|
+
mime: z.string().optional().describe("MIME type for a base64 image (e.g. \"image/png\"). Defaults to image/png.")
|
|
6351
|
+
})).optional().describe("Optional vision input. Each image attaches to the last user message and is mapped to the provider's native image format. The prompt's model must be vision-capable (e.g. claude-sonnet-4-5, gpt-4o, gemini-2.5-pro)."),
|
|
5793
6352
|
extra_body: z.record(z.unknown()).optional().describe("Optional extra fields merged into the upstream body. Allowlisted keys only: temperature, top_p, top_k, stop, stop_sequences, response_format, frequency_penalty, presence_penalty, seed, tools, tool_choice, metadata. Unrecognised keys are rejected to prevent provider-body smuggling (e.g. authorization, model overrides).")
|
|
5794
|
-
}, async ({ project_id, name, messages, extra_body }, { pat }) => {
|
|
6353
|
+
}, async ({ project_id, name, messages, images, extra_body }, { pat }) => {
|
|
5795
6354
|
const prompt = (await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/ai/prompts/${encodeURIComponent(name)}`) ?? {}).data;
|
|
5796
6355
|
if (!prompt) throw new Error(`Prompt "${name}" not found.`);
|
|
5797
|
-
const
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
6356
|
+
const hasImagesParam = Array.isArray(images) && images.length > 0;
|
|
6357
|
+
let body;
|
|
6358
|
+
if (prompt.provider === "gemini") {
|
|
6359
|
+
body = {
|
|
6360
|
+
model: prompt.model,
|
|
6361
|
+
contents: geminiContentsFromMessages(messages, images ?? []),
|
|
6362
|
+
generationConfig: { maxOutputTokens: prompt.max_tokens }
|
|
6363
|
+
};
|
|
6364
|
+
if (prompt.system_prompt) body.systemInstruction = { parts: [{ text: prompt.system_prompt }] };
|
|
6365
|
+
} else {
|
|
6366
|
+
const messagesHaveImageBlocks = messagesHaveNeutralImage(messages);
|
|
6367
|
+
const effectiveMessages = hasImagesParam || messagesHaveImageBlocks ? attachImagesToMessages(messages, images ?? [], prompt.provider) : messages;
|
|
6368
|
+
body = {
|
|
6369
|
+
model: prompt.model,
|
|
6370
|
+
max_tokens: prompt.max_tokens
|
|
6371
|
+
};
|
|
6372
|
+
if (prompt.provider === "anthropic") {
|
|
6373
|
+
if (prompt.system_prompt) body.system = prompt.system_prompt;
|
|
6374
|
+
body.messages = effectiveMessages;
|
|
6375
|
+
} else body.messages = prompt.system_prompt ? [{
|
|
6376
|
+
role: "system",
|
|
6377
|
+
content: prompt.system_prompt
|
|
6378
|
+
}, ...effectiveMessages] : effectiveMessages;
|
|
6379
|
+
}
|
|
5808
6380
|
if (extra_body) {
|
|
5809
6381
|
const ALLOWED_EXTRA_KEYS = new Set([
|
|
5810
6382
|
"temperature",
|
|
@@ -5820,16 +6392,67 @@ function registerTools$7(server, apiClient) {
|
|
|
5820
6392
|
"tool_choice",
|
|
5821
6393
|
"metadata"
|
|
5822
6394
|
]);
|
|
5823
|
-
for (const
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
6395
|
+
for (const k of Object.keys(extra_body)) if (!ALLOWED_EXTRA_KEYS.has(k)) throw new Error(`extra_body key "${k}" is not allowed. Allowlist: ${Array.from(ALLOWED_EXTRA_KEYS).join(", ")}`);
|
|
6396
|
+
if (prompt.provider === "gemini") {
|
|
6397
|
+
const gc = body.generationConfig ?? {};
|
|
6398
|
+
const GEMINI_KEY = {
|
|
6399
|
+
temperature: "temperature",
|
|
6400
|
+
top_p: "topP",
|
|
6401
|
+
top_k: "topK",
|
|
6402
|
+
seed: "seed",
|
|
6403
|
+
frequency_penalty: "frequencyPenalty",
|
|
6404
|
+
presence_penalty: "presencePenalty"
|
|
6405
|
+
};
|
|
6406
|
+
for (const [k, v] of Object.entries(extra_body)) if (k === "stop" || k === "stop_sequences") gc.stopSequences = Array.isArray(v) ? v : [v];
|
|
6407
|
+
else if (k === "response_format") {
|
|
6408
|
+
const rf = typeof v === "object" && v !== null ? v : null;
|
|
6409
|
+
const rfType = rf?.["type"];
|
|
6410
|
+
if (rfType === "json_object") gc.responseMimeType = "application/json";
|
|
6411
|
+
else if (rfType === "json_schema") {
|
|
6412
|
+
gc.responseMimeType = "application/json";
|
|
6413
|
+
const schema = rf && typeof rf["schema"] === "object" && rf["schema"] !== null ? rf["schema"] : void 0;
|
|
6414
|
+
if (schema !== void 0) gc.responseSchema = schema;
|
|
6415
|
+
}
|
|
6416
|
+
} else if (k in GEMINI_KEY) gc[GEMINI_KEY[k]] = v;
|
|
6417
|
+
else body[k] = v;
|
|
6418
|
+
body.generationConfig = gc;
|
|
6419
|
+
} else for (const [k, v] of Object.entries(extra_body)) body[k] = v;
|
|
5827
6420
|
}
|
|
5828
6421
|
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/ai/messages`, { body: {
|
|
5829
6422
|
provider: prompt.provider,
|
|
5830
6423
|
body
|
|
5831
6424
|
} }));
|
|
5832
6425
|
}, ["amba_invoke_ai_prompt"]);
|
|
6426
|
+
registerTool(server, apiClient, "amba_ai_prompts_set_budget", [
|
|
6427
|
+
"Set (or clear) a per-prompt AI spend budget. Caps how much this one prompt can spend on model calls per period; once the period's spend reaches the budget, invocations are denied with a clear `ai_budget_exceeded` error until the period resets — so an exposed prompt can't burn unbounded spend.",
|
|
6428
|
+
"Pass `budget_usd` as a dollar amount (e.g. 25 = $25). Pass `budget_usd: null` to remove the budget (unlimited — the default). `budget_period` is the reset cadence: \"monthly\" (default), \"daily\", or \"total\" (never resets).",
|
|
6429
|
+
"Returns the persisted budget. For the whole-project ceiling instead, use `amba_billing_set_ceiling`."
|
|
6430
|
+
].join(" "), {
|
|
6431
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6432
|
+
name: z.string().describe("Registered prompt name to budget."),
|
|
6433
|
+
budget_usd: z.number().min(0).max(1e6).nullable().describe("Per-period USD ceiling. Pass null to clear the budget (unlimited)."),
|
|
6434
|
+
budget_period: z.enum([
|
|
6435
|
+
"daily",
|
|
6436
|
+
"monthly",
|
|
6437
|
+
"total"
|
|
6438
|
+
]).optional().describe("Reset cadence. Defaults to \"monthly\" when a budget is set.")
|
|
6439
|
+
}, async ({ project_id, name, budget_usd, budget_period }, { pat }) => {
|
|
6440
|
+
const body = { budget_usd };
|
|
6441
|
+
if (budget_period !== void 0) body.budget_period = budget_period;
|
|
6442
|
+
return jsonResult(await callWithPat(apiClient, pat, "PUT", `/projects/${encodeURIComponent(project_id)}/ai/prompts/${encodeURIComponent(name)}/budget`, { body }));
|
|
6443
|
+
});
|
|
6444
|
+
registerTool(server, apiClient, "amba_ai_prompts_get_spend", ["Read a prompt's AI spend for the current budget period, alongside its budget (if any). Returns { name, period, period_start, spend_usd, budget_usd, exceeded }.", "Spend is summed from the per-call cost of every invocation in the period. Use this to check headroom before raising a budget, or to diagnose an `ai_budget_exceeded` denial. Pass `period` to inspect a different window (defaults to the prompt's configured period, or \"monthly\")."].join(" "), {
|
|
6445
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6446
|
+
name: z.string().describe("Registered prompt name."),
|
|
6447
|
+
period: z.enum([
|
|
6448
|
+
"daily",
|
|
6449
|
+
"monthly",
|
|
6450
|
+
"total"
|
|
6451
|
+
]).optional().describe("Spend window to report. Defaults to the prompt's configured budget period.")
|
|
6452
|
+
}, async ({ project_id, name, period }, { pat }) => {
|
|
6453
|
+
const query = period ? `?period=${encodeURIComponent(period)}` : "";
|
|
6454
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/ai/prompts/${encodeURIComponent(name)}/spend${query}`));
|
|
6455
|
+
});
|
|
5833
6456
|
}
|
|
5834
6457
|
//#endregion
|
|
5835
6458
|
//#region src/tools/ai-providers-admin.ts
|
|
@@ -5839,7 +6462,7 @@ const PROVIDER_ENUM = z.enum([
|
|
|
5839
6462
|
"mistral",
|
|
5840
6463
|
"gemini"
|
|
5841
6464
|
]);
|
|
5842
|
-
function registerTools$
|
|
6465
|
+
function registerTools$7(server, apiClient) {
|
|
5843
6466
|
registerTool(server, apiClient, "amba_ai_providers_set", [
|
|
5844
6467
|
"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
6468
|
"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.",
|
|
@@ -5879,7 +6502,7 @@ const STATUS_ENUM = z.enum([
|
|
|
5879
6502
|
"paused",
|
|
5880
6503
|
"ended"
|
|
5881
6504
|
]).describe("active = assigns + exposes new users; paused = serves existing assignments but issues no new ones; ended = read-only.");
|
|
5882
|
-
function registerTools$
|
|
6505
|
+
function registerTools$6(server, apiClient) {
|
|
5883
6506
|
registerTool(server, apiClient, "amba_experiments_create", [
|
|
5884
6507
|
"Create an A/B experiment: a stable `key`, a display `name`, and >= 2 weighted variants.",
|
|
5885
6508
|
"End-users are bucketed into a STICKY variant the first time they request an assignment",
|
|
@@ -5982,7 +6605,7 @@ function registerTools$5(server, apiClient) {
|
|
|
5982
6605
|
}
|
|
5983
6606
|
//#endregion
|
|
5984
6607
|
//#region src/tools/promotion.ts
|
|
5985
|
-
function registerTools$
|
|
6608
|
+
function registerTools$5(server, apiClient) {
|
|
5986
6609
|
registerTool(server, apiClient, "amba_project_export", [
|
|
5987
6610
|
"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
6611
|
"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.",
|
|
@@ -6088,7 +6711,7 @@ const TIER_CATALOG = {
|
|
|
6088
6711
|
note: "Use Amba.track(name, props, { telemetry: true }) for high-volume telemetry. Cheaper, no fan-out to segments/workflows/push."
|
|
6089
6712
|
}
|
|
6090
6713
|
};
|
|
6091
|
-
function registerTools$
|
|
6714
|
+
function registerTools$4(server, apiClient) {
|
|
6092
6715
|
registerTool(server, apiClient, "amba_billing_status", "Get the current billing tier, headroom on each metered axis (MAU, events, push, db_storage, media_storage), projected overage for the current period, spend ceiling, and what (if any) human action is required before the agent can continue safely. Use this BEFORE provisioning new features or running data-heavy workloads so the agent can self-throttle or escalate to the human.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
6093
6716
|
const result = await client.get(`/projects/${project_id}/billing/status`);
|
|
6094
6717
|
return { content: [{
|
|
@@ -6114,6 +6737,80 @@ function registerTools$3(server, apiClient) {
|
|
|
6114
6737
|
});
|
|
6115
6738
|
}
|
|
6116
6739
|
//#endregion
|
|
6740
|
+
//#region src/tools/payments.ts
|
|
6741
|
+
function registerTools$3(server, apiClient) {
|
|
6742
|
+
registerTool(server, apiClient, "amba_payments_account_create", "Create the project's connected payments account so the app can accept payments from its users (the app is the seller; Amba takes a platform fee, Stripe carries the money). Idempotent — returns the existing account if one is already set up. After this, call amba_payments_onboarding_link to finish setup. Note: payments must be enabled on the Amba platform by the platform owner before live accounts can be created.", {
|
|
6743
|
+
project_id: z.string().describe("The project ID"),
|
|
6744
|
+
country: z.string().length(2).optional().describe("Two-letter ISO country code for the seller (e.g. \"US\")"),
|
|
6745
|
+
email: z.string().optional().describe("Contact email for the connected account")
|
|
6746
|
+
}, async ({ project_id, country, email }, { client }) => {
|
|
6747
|
+
const result = await client.post(`/projects/${project_id}/payments/accounts`, {
|
|
6748
|
+
...country ? { country } : {},
|
|
6749
|
+
...email ? { email } : {}
|
|
6750
|
+
});
|
|
6751
|
+
return { content: [{
|
|
6752
|
+
type: "text",
|
|
6753
|
+
text: JSON.stringify(result, null, 2)
|
|
6754
|
+
}] };
|
|
6755
|
+
});
|
|
6756
|
+
registerTool(server, apiClient, "amba_payments_create_onboarding_link", "Generate a hosted onboarding URL the seller opens to finish setting up payments (identity, bank details — handled entirely by the payment provider, Amba never sees identity documents). Surface this URL to the human as a copy box. The link is single-use and short-lived; generate a fresh one if it expires.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
6757
|
+
const result = await client.post(`/projects/${project_id}/payments/accounts/link`, {});
|
|
6758
|
+
return { content: [{
|
|
6759
|
+
type: "text",
|
|
6760
|
+
text: JSON.stringify(result, null, 2)
|
|
6761
|
+
}] };
|
|
6762
|
+
});
|
|
6763
|
+
registerTool(server, apiClient, "amba_payments_account_status", "Read the connected payments account status — onboarding stage, whether charges and payouts are enabled, and the configured default platform fee. Returns connected_account: null when payments are not set up for the project yet. Check this before attempting a charge.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
6764
|
+
const result = await client.get(`/projects/${project_id}/payments/accounts`);
|
|
6765
|
+
return { content: [{
|
|
6766
|
+
type: "text",
|
|
6767
|
+
text: JSON.stringify(result, null, 2)
|
|
6768
|
+
}] };
|
|
6769
|
+
});
|
|
6770
|
+
registerTool(server, apiClient, "amba_payments_set_fee", "Set the default platform fee taken on each payment, in basis points (e.g. 250 = 2.5%). Applied when a charge does not specify its own fee. Pass null to clear the default (each charge must then specify a fee explicitly). This is a money-affecting setting — surface it to the human before calling.", {
|
|
6771
|
+
project_id: z.string().describe("The project ID"),
|
|
6772
|
+
default_platform_fee_bps: z.number().int().min(0).max(1e4).nullable().describe("Platform fee in basis points (250 = 2.5%), or null to clear")
|
|
6773
|
+
}, async ({ project_id, default_platform_fee_bps }, { client }) => {
|
|
6774
|
+
const result = await client.put(`/projects/${project_id}/payments/config`, { default_platform_fee_bps });
|
|
6775
|
+
return { content: [{
|
|
6776
|
+
type: "text",
|
|
6777
|
+
text: JSON.stringify(result, null, 2)
|
|
6778
|
+
}] };
|
|
6779
|
+
});
|
|
6780
|
+
registerTool(server, apiClient, "amba_payments_charge", "Create a payment (destination charge) on behalf of the seller, taking the platform fee. amount is in the smallest currency unit (cents). The fee is the platform fee (application_fee_amount) — provide application_fee_amount (cents) OR platform_fee_bps (basis points), else the project default is used. Returns a client_secret the app uses to confirm the payment on-device. Money-moving action — surface to the human before calling.", {
|
|
6781
|
+
project_id: z.string().describe("The project ID"),
|
|
6782
|
+
amount: z.number().int().positive().describe("Amount in smallest currency unit (cents)"),
|
|
6783
|
+
currency: z.string().length(3).describe("3-letter ISO currency code (lowercase, e.g. \"usd\")"),
|
|
6784
|
+
application_fee_amount: z.number().int().min(0).optional().describe("Platform fee in cents (overrides bps/default)"),
|
|
6785
|
+
platform_fee_bps: z.number().int().min(0).max(1e4).optional().describe("Platform fee in basis points (250 = 2.5%)"),
|
|
6786
|
+
client_reference: z.string().optional().describe("Opaque reference (e.g. your order id) stored with the payment")
|
|
6787
|
+
}, async ({ project_id, ...body }, { client }) => {
|
|
6788
|
+
const result = await client.post(`/projects/${project_id}/payments/charges`, body);
|
|
6789
|
+
return { content: [{
|
|
6790
|
+
type: "text",
|
|
6791
|
+
text: JSON.stringify(result, null, 2)
|
|
6792
|
+
}] };
|
|
6793
|
+
});
|
|
6794
|
+
registerTool(server, apiClient, "amba_payments_balance", "Read the seller's available and pending payment balance per currency. Use this to show the developer how much they've earned that hasn't yet been paid out.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
6795
|
+
const result = await client.get(`/projects/${project_id}/payments/balance`);
|
|
6796
|
+
return { content: [{
|
|
6797
|
+
type: "text",
|
|
6798
|
+
text: JSON.stringify(result, null, 2)
|
|
6799
|
+
}] };
|
|
6800
|
+
});
|
|
6801
|
+
registerTool(server, apiClient, "amba_payments_payouts", "List the seller's recent payouts (transfers from the payment balance to their bank), with amount, currency, status, and arrival date.", {
|
|
6802
|
+
project_id: z.string().describe("The project ID"),
|
|
6803
|
+
limit: z.number().int().min(1).max(100).optional().describe("How many payouts to return (default 10)")
|
|
6804
|
+
}, async ({ project_id, limit }, { client }) => {
|
|
6805
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
6806
|
+
const result = await client.get(`/projects/${project_id}/payments/payouts${qs}`);
|
|
6807
|
+
return { content: [{
|
|
6808
|
+
type: "text",
|
|
6809
|
+
text: JSON.stringify(result, null, 2)
|
|
6810
|
+
}] };
|
|
6811
|
+
});
|
|
6812
|
+
}
|
|
6813
|
+
//#endregion
|
|
6117
6814
|
//#region src/tools/invites.ts
|
|
6118
6815
|
function registerTools$2(server, apiClient) {
|
|
6119
6816
|
registerTool(server, apiClient, "amba_projects_invite_member", "Invite a teammate by email to collaborate on this project. Returns the invite URL the human should send to their teammate. Roles: admin (can manage members + integrations), member (can write data), viewer (read-only).", {
|
|
@@ -8279,6 +8976,9 @@ const TOOL_CATEGORY = {
|
|
|
8279
8976
|
amba_content_list_items: "engagement",
|
|
8280
8977
|
amba_content_update_item: "engagement",
|
|
8281
8978
|
amba_content_delete_item: "engagement",
|
|
8979
|
+
amba_content_translations_set: "engagement",
|
|
8980
|
+
amba_content_translations_list: "engagement",
|
|
8981
|
+
amba_content_translations_delete: "engagement",
|
|
8282
8982
|
amba_content_bulk_import: "engagement",
|
|
8283
8983
|
amba_content_schedules_create: "engagement",
|
|
8284
8984
|
amba_create_content_schedule: "engagement",
|
|
@@ -8361,6 +9061,16 @@ const TOOL_CATEGORY = {
|
|
|
8361
9061
|
amba_delete_challenge: "gamification",
|
|
8362
9062
|
amba_challenges_list_participants: "gamification",
|
|
8363
9063
|
amba_list_challenge_participants: "gamification",
|
|
9064
|
+
amba_monetization_plan: "economy",
|
|
9065
|
+
amba_monetization_drift: "economy",
|
|
9066
|
+
amba_monetization_export: "economy",
|
|
9067
|
+
amba_monetization_definitions_list: "economy",
|
|
9068
|
+
amba_monetization_adopt: "economy",
|
|
9069
|
+
amba_entitlements_define: "economy",
|
|
9070
|
+
amba_products_create: "economy",
|
|
9071
|
+
amba_entitlements_map_product: "economy",
|
|
9072
|
+
amba_offerings_create: "economy",
|
|
9073
|
+
amba_offerings_list: "economy",
|
|
8364
9074
|
amba_currencies_create: "economy",
|
|
8365
9075
|
amba_create_currency: "economy",
|
|
8366
9076
|
amba_currencies_list: "economy",
|
|
@@ -8470,6 +9180,7 @@ const TOOL_CATEGORY = {
|
|
|
8470
9180
|
amba_events_list: "analytics",
|
|
8471
9181
|
amba_events_count: "analytics",
|
|
8472
9182
|
amba_events_track: "analytics",
|
|
9183
|
+
amba_events_explain: "analytics",
|
|
8473
9184
|
amba_analytics_get: "analytics",
|
|
8474
9185
|
amba_get_analytics: "analytics",
|
|
8475
9186
|
amba_sessions_list: "analytics",
|
|
@@ -8575,6 +9286,13 @@ const TOOL_CATEGORY = {
|
|
|
8575
9286
|
amba_media_create_folder: "infrastructure",
|
|
8576
9287
|
amba_media_delete_folder: "infrastructure",
|
|
8577
9288
|
amba_media_list_folders: "infrastructure",
|
|
9289
|
+
amba_media_catalogs_create: "infrastructure",
|
|
9290
|
+
amba_media_catalogs_list: "infrastructure",
|
|
9291
|
+
amba_media_catalogs_get: "infrastructure",
|
|
9292
|
+
amba_media_catalogs_update: "infrastructure",
|
|
9293
|
+
amba_media_catalogs_delete: "infrastructure",
|
|
9294
|
+
amba_media_catalogs_add_item: "infrastructure",
|
|
9295
|
+
amba_media_catalogs_remove_item: "infrastructure",
|
|
8578
9296
|
amba_secrets_set: "infrastructure",
|
|
8579
9297
|
amba_set_secret: "infrastructure",
|
|
8580
9298
|
amba_secrets_get: "infrastructure",
|
|
@@ -8613,11 +9331,20 @@ const TOOL_CATEGORY = {
|
|
|
8613
9331
|
amba_delete_ai_prompt: "infrastructure",
|
|
8614
9332
|
amba_ai_prompts_invoke: "infrastructure",
|
|
8615
9333
|
amba_invoke_ai_prompt: "infrastructure",
|
|
9334
|
+
amba_ai_prompts_set_budget: "infrastructure",
|
|
9335
|
+
amba_ai_prompts_get_spend: "infrastructure",
|
|
8616
9336
|
amba_sdk_get_setup_instructions: "infrastructure",
|
|
8617
9337
|
amba_get_sdk_setup_instructions: "infrastructure",
|
|
8618
9338
|
amba_billing_status: "infrastructure",
|
|
8619
9339
|
amba_billing_tiers: "infrastructure",
|
|
8620
9340
|
amba_billing_set_ceiling: "infrastructure",
|
|
9341
|
+
amba_payments_account_create: "economy",
|
|
9342
|
+
amba_payments_create_onboarding_link: "economy",
|
|
9343
|
+
amba_payments_account_status: "economy",
|
|
9344
|
+
amba_payments_set_fee: "economy",
|
|
9345
|
+
amba_payments_charge: "economy",
|
|
9346
|
+
amba_payments_balance: "economy",
|
|
9347
|
+
amba_payments_payouts: "economy",
|
|
8621
9348
|
amba_projects_invite_member: "infrastructure",
|
|
8622
9349
|
amba_projects_list_members: "infrastructure",
|
|
8623
9350
|
amba_projects_remove_member: "infrastructure",
|
|
@@ -8664,6 +9391,691 @@ function getToolCategory(toolName) {
|
|
|
8664
9391
|
return TOOL_CATEGORY[toolName] ?? null;
|
|
8665
9392
|
}
|
|
8666
9393
|
//#endregion
|
|
9394
|
+
//#region src/auto/schema-to-zod.ts
|
|
9395
|
+
/**
|
|
9396
|
+
* Pure mapper: a collection's live schema (the JSON returned by the
|
|
9397
|
+
* `GET .../collections/:name` describe endpoint) → the zod raw shapes the
|
|
9398
|
+
* auto-generated MCP tools register.
|
|
9399
|
+
*
|
|
9400
|
+
* Three derived shapes per collection (the auto-MCP wedge §2.2):
|
|
9401
|
+
*
|
|
9402
|
+
* - `insertShape` — the typed body of an `insert` tool. Server-managed
|
|
9403
|
+
* columns (`id`, `user_id`, `created_at`, `updated_at`, `deleted_at`)
|
|
9404
|
+
* are omitted (the route stamps them); a column is REQUIRED when it is
|
|
9405
|
+
* NOT NULL and has no default, OPTIONAL otherwise.
|
|
9406
|
+
* - `whereShape` — the typed `where` of a `find` tool: one optional key
|
|
9407
|
+
* per non-vector column whose value is `value | { eq?, ne?, gt?, … }`
|
|
9408
|
+
* typed to the column. This is the single highest-value projection —
|
|
9409
|
+
* it makes the generated tool read like a hand-built typed client
|
|
9410
|
+
* rather than a free-form `where: object` blob.
|
|
9411
|
+
* - `updateShape` — the typed `set` of an `update` tool: every non
|
|
9412
|
+
* server-managed column, all optional (merge-patch semantics).
|
|
9413
|
+
*
|
|
9414
|
+
* Plus `rowTypeHints` — a column→plain-English type label map the
|
|
9415
|
+
* description generator (`describe.ts`) reads, and `vectorColumns` — the
|
|
9416
|
+
* vector columns (excluded from row input; they drive `find_nearest`).
|
|
9417
|
+
*
|
|
9418
|
+
* The mapper is PURE (no I/O, no zod-runtime coupling beyond building the
|
|
9419
|
+
* shape) so it unit-tests in isolation and so the same projection can run
|
|
9420
|
+
* in the hosted server today and in the CLI / console later.
|
|
9421
|
+
*/
|
|
9422
|
+
/**
|
|
9423
|
+
* Columns Amba manages on every collection. They are stamped by the
|
|
9424
|
+
* route, never accepted as tool input, and so are omitted from the
|
|
9425
|
+
* insert / update shapes. They ARE filterable (`where`) and selectable.
|
|
9426
|
+
*/
|
|
9427
|
+
const SERVER_MANAGED_COLUMNS = new Set([
|
|
9428
|
+
"id",
|
|
9429
|
+
"user_id",
|
|
9430
|
+
"created_at",
|
|
9431
|
+
"updated_at",
|
|
9432
|
+
"deleted_at"
|
|
9433
|
+
]);
|
|
9434
|
+
/**
|
|
9435
|
+
* Classify a column into a coarse {@link ColumnKind}. Reads `data_type`
|
|
9436
|
+
* first (the only reliable array signal — `data_type === 'ARRAY'`), then
|
|
9437
|
+
* `udt_name`. Unknown types fall back to `unknown` (mapped to free JSON)
|
|
9438
|
+
* rather than throwing, so a future column type never breaks generation.
|
|
9439
|
+
*/
|
|
9440
|
+
function classifyColumn(column) {
|
|
9441
|
+
const udt = column.udt_name.toLowerCase();
|
|
9442
|
+
if (column.data_type.toUpperCase() === "ARRAY" || udt.startsWith("_")) switch (udt.replace(/^_/, "")) {
|
|
9443
|
+
case "text":
|
|
9444
|
+
case "varchar":
|
|
9445
|
+
case "bpchar": return "text[]";
|
|
9446
|
+
case "int2":
|
|
9447
|
+
case "int4":
|
|
9448
|
+
case "int8": return "integer[]";
|
|
9449
|
+
case "numeric":
|
|
9450
|
+
case "float4":
|
|
9451
|
+
case "float8": return "number[]";
|
|
9452
|
+
case "bool": return "boolean[]";
|
|
9453
|
+
case "uuid": return "uuid[]";
|
|
9454
|
+
default: return "unknown";
|
|
9455
|
+
}
|
|
9456
|
+
switch (udt) {
|
|
9457
|
+
case "text":
|
|
9458
|
+
case "varchar":
|
|
9459
|
+
case "bpchar": return "text";
|
|
9460
|
+
case "int2":
|
|
9461
|
+
case "int4":
|
|
9462
|
+
case "int8": return "integer";
|
|
9463
|
+
case "numeric":
|
|
9464
|
+
case "float4":
|
|
9465
|
+
case "float8": return "number";
|
|
9466
|
+
case "bool": return "boolean";
|
|
9467
|
+
case "jsonb":
|
|
9468
|
+
case "json": return "json";
|
|
9469
|
+
case "uuid": return "uuid";
|
|
9470
|
+
case "date": return "date";
|
|
9471
|
+
case "timestamptz":
|
|
9472
|
+
case "timestamp": return "timestamp";
|
|
9473
|
+
case "vector": return "vector";
|
|
9474
|
+
default: return "unknown";
|
|
9475
|
+
}
|
|
9476
|
+
}
|
|
9477
|
+
/** Build the base (scalar/array) zod type for a column kind. */
|
|
9478
|
+
function baseTypeForKind(kind) {
|
|
9479
|
+
switch (kind) {
|
|
9480
|
+
case "text": return z.string();
|
|
9481
|
+
case "uuid": return z.string().describe("uuid");
|
|
9482
|
+
case "date": return z.string().describe("ISO-8601 date (YYYY-MM-DD)");
|
|
9483
|
+
case "timestamp": return z.string().describe("ISO-8601 timestamp");
|
|
9484
|
+
case "integer": return z.number().int();
|
|
9485
|
+
case "number": return z.number();
|
|
9486
|
+
case "boolean": return z.boolean();
|
|
9487
|
+
case "json": return z.unknown();
|
|
9488
|
+
case "text[]": return z.array(z.string());
|
|
9489
|
+
case "integer[]": return z.array(z.number().int());
|
|
9490
|
+
case "number[]": return z.array(z.number());
|
|
9491
|
+
case "boolean[]": return z.array(z.boolean());
|
|
9492
|
+
case "uuid[]": return z.array(z.string());
|
|
9493
|
+
case "vector": return z.array(z.number());
|
|
9494
|
+
default: return z.unknown();
|
|
9495
|
+
}
|
|
9496
|
+
}
|
|
9497
|
+
/**
|
|
9498
|
+
* Build the per-column `where` operator object. Every operator is
|
|
9499
|
+
* optional; the value type matches the column. A bare value is also
|
|
9500
|
+
* accepted as shorthand for `{ eq: value }` (mirrors the server, which
|
|
9501
|
+
* treats a non-object field value as equality).
|
|
9502
|
+
*/
|
|
9503
|
+
function whereTypeForKind(kind, columnName) {
|
|
9504
|
+
const base = baseTypeForKind(kind);
|
|
9505
|
+
const isArray = kind.endsWith("[]");
|
|
9506
|
+
const isComparable = kind === "integer" || kind === "number" || kind === "date" || kind === "timestamp" || kind === "text";
|
|
9507
|
+
const isText = kind === "text";
|
|
9508
|
+
const ops = {
|
|
9509
|
+
eq: base.optional(),
|
|
9510
|
+
ne: base.optional(),
|
|
9511
|
+
in: z.array(base).optional(),
|
|
9512
|
+
notIn: z.array(base).optional(),
|
|
9513
|
+
isNull: z.boolean().optional(),
|
|
9514
|
+
isNotNull: z.boolean().optional()
|
|
9515
|
+
};
|
|
9516
|
+
if (isComparable) {
|
|
9517
|
+
ops.gt = base.optional();
|
|
9518
|
+
ops.gte = base.optional();
|
|
9519
|
+
ops.lt = base.optional();
|
|
9520
|
+
ops.lte = base.optional();
|
|
9521
|
+
}
|
|
9522
|
+
if (isText) {
|
|
9523
|
+
ops.like = z.string().optional().describe("SQL LIKE pattern (case-sensitive).");
|
|
9524
|
+
ops.ilike = z.string().optional().describe("SQL ILIKE pattern (case-insensitive).");
|
|
9525
|
+
}
|
|
9526
|
+
if (isArray) {
|
|
9527
|
+
ops.contains = base.optional().describe("Rows whose array contains all of these elements.");
|
|
9528
|
+
ops.containedBy = base.optional().describe("Rows whose array is contained by these elements.");
|
|
9529
|
+
ops.overlaps = base.optional().describe("Rows whose array shares any element with these.");
|
|
9530
|
+
}
|
|
9531
|
+
return z.union([z.object(ops), base]).optional().describe(`Filter on \`${columnName}\`. Bare value = equality; or an operator object.`);
|
|
9532
|
+
}
|
|
9533
|
+
/**
|
|
9534
|
+
* Whether a column is required on insert: NOT NULL and no column default.
|
|
9535
|
+
* A defaulted NOT NULL column is optional (the DB fills it).
|
|
9536
|
+
*/
|
|
9537
|
+
function isRequiredOnInsert(column) {
|
|
9538
|
+
return column.is_nullable === "NO" && column.column_default === null;
|
|
9539
|
+
}
|
|
9540
|
+
/**
|
|
9541
|
+
* Project a collection schema into the zod raw shapes the auto-MCP tools
|
|
9542
|
+
* register. Pure — same input always yields the same shapes.
|
|
9543
|
+
*/
|
|
9544
|
+
function mapCollectionSchema(schema, options = {}) {
|
|
9545
|
+
const insertShape = {};
|
|
9546
|
+
const whereShape = {};
|
|
9547
|
+
const updateShape = {};
|
|
9548
|
+
const rowTypeHints = {};
|
|
9549
|
+
const vectorColumns = [];
|
|
9550
|
+
for (const column of schema.columns) {
|
|
9551
|
+
const name = column.column_name;
|
|
9552
|
+
const kind = classifyColumn(column);
|
|
9553
|
+
rowTypeHints[name] = kind;
|
|
9554
|
+
if (kind === "vector") {
|
|
9555
|
+
vectorColumns.push(name);
|
|
9556
|
+
continue;
|
|
9557
|
+
}
|
|
9558
|
+
whereShape[name] = whereTypeForKind(kind, name);
|
|
9559
|
+
if (SERVER_MANAGED_COLUMNS.has(name)) {
|
|
9560
|
+
if (name === "user_id" && options.includeUserIdInInsert) insertShape[name] = baseTypeForKind(kind).optional().describe("Optional. The app user id to attribute this row to. Omit to insert without an owner (requires a shared collection) or use as_system.");
|
|
9561
|
+
continue;
|
|
9562
|
+
}
|
|
9563
|
+
const base = baseTypeForKind(kind);
|
|
9564
|
+
updateShape[name] = base.optional();
|
|
9565
|
+
insertShape[name] = isRequiredOnInsert(column) ? base : base.optional();
|
|
9566
|
+
}
|
|
9567
|
+
return {
|
|
9568
|
+
insertShape,
|
|
9569
|
+
whereShape,
|
|
9570
|
+
updateShape,
|
|
9571
|
+
rowTypeHints,
|
|
9572
|
+
vectorColumns,
|
|
9573
|
+
hasVector: vectorColumns.length > 0
|
|
9574
|
+
};
|
|
9575
|
+
}
|
|
9576
|
+
//#endregion
|
|
9577
|
+
//#region src/auto/describe.ts
|
|
9578
|
+
/**
|
|
9579
|
+
* SDK-grade per-tool description generator for the auto-MCP wedge.
|
|
9580
|
+
*
|
|
9581
|
+
* The wedge is only worth shipping if the generated tools read like a
|
|
9582
|
+
* hand-built typed client, not a thin `find_rows({collection, where})`
|
|
9583
|
+
* wrapper (design doc §7). These descriptions are generated from the live
|
|
9584
|
+
* column list + access policy so an agent never needs external docs:
|
|
9585
|
+
* column names, types, nullability, the access policy in plain words, and
|
|
9586
|
+
* a worked example per verb.
|
|
9587
|
+
*
|
|
9588
|
+
* Pure string builders — snapshot-friendly, no I/O. NO provider leakage:
|
|
9589
|
+
* "collection", "row", "column", "function" only — never the underlying
|
|
9590
|
+
* storage engine.
|
|
9591
|
+
*/
|
|
9592
|
+
/** A human-friendly type label for a column kind. */
|
|
9593
|
+
function labelForKind(kind) {
|
|
9594
|
+
switch (kind) {
|
|
9595
|
+
case "integer": return "integer";
|
|
9596
|
+
case "number": return "number";
|
|
9597
|
+
case "boolean": return "boolean";
|
|
9598
|
+
case "json": return "JSON";
|
|
9599
|
+
case "uuid": return "uuid (string)";
|
|
9600
|
+
case "date": return "date string";
|
|
9601
|
+
case "timestamp": return "timestamp string";
|
|
9602
|
+
case "vector": return "vector";
|
|
9603
|
+
case "text[]": return "string array";
|
|
9604
|
+
case "integer[]": return "integer array";
|
|
9605
|
+
case "number[]": return "number array";
|
|
9606
|
+
case "boolean[]": return "boolean array";
|
|
9607
|
+
case "uuid[]": return "uuid array";
|
|
9608
|
+
case "text": return "text";
|
|
9609
|
+
default: return "value";
|
|
9610
|
+
}
|
|
9611
|
+
}
|
|
9612
|
+
/**
|
|
9613
|
+
* One-line summary of the collection's columns an agent can write to
|
|
9614
|
+
* (insert/update). Server-managed columns are excluded — the route stamps
|
|
9615
|
+
* them.
|
|
9616
|
+
*/
|
|
9617
|
+
function writableColumnSummary(mapped) {
|
|
9618
|
+
const cols = Object.keys(mapped.rowTypeHints).filter((c) => !SERVER_MANAGED_COLUMNS.has(c) && mapped.rowTypeHints[c] !== "vector");
|
|
9619
|
+
if (cols.length === 0) return "(no writable columns)";
|
|
9620
|
+
return cols.map((c) => `${c} (${labelForKind(mapped.rowTypeHints[c])})`).join(", ");
|
|
9621
|
+
}
|
|
9622
|
+
/** All columns with types, for the find/list filter description. */
|
|
9623
|
+
function allColumnSummary(mapped) {
|
|
9624
|
+
const cols = Object.keys(mapped.rowTypeHints);
|
|
9625
|
+
if (cols.length === 0) return "(no columns)";
|
|
9626
|
+
return cols.map((c) => `${c} (${labelForKind(mapped.rowTypeHints[c])})`).join(", ");
|
|
9627
|
+
}
|
|
9628
|
+
/** Plain-words access-policy sentence for the given scope. */
|
|
9629
|
+
function policySentence(ctx) {
|
|
9630
|
+
if (ctx.scope === "admin") return "Developer/admin scope: sees and mutates EVERY row regardless of which app user owns it (no per-user scoping).";
|
|
9631
|
+
return `End-user scope: ${ctx.readPolicy === "public" ? "Reads return all rows (this collection is public-read)." : "Reads return only the signed-in user's own rows (plus shared/unowned rows)."} ${ctx.writePolicy === "authenticated" ? "Any signed-in user may write." : "Writes affect only the signed-in user's own rows."}`;
|
|
9632
|
+
}
|
|
9633
|
+
/** Build the first writable column name for worked examples. */
|
|
9634
|
+
function exampleWritableColumn(mapped) {
|
|
9635
|
+
return Object.keys(mapped.rowTypeHints).find((c) => !SERVER_MANAGED_COLUMNS.has(c) && mapped.rowTypeHints[c] !== "vector") ?? null;
|
|
9636
|
+
}
|
|
9637
|
+
/** Example value literal for a column kind (for worked examples). */
|
|
9638
|
+
function exampleValue(kind) {
|
|
9639
|
+
switch (kind) {
|
|
9640
|
+
case "integer": return "42";
|
|
9641
|
+
case "number": return "3.14";
|
|
9642
|
+
case "boolean": return "true";
|
|
9643
|
+
case "json": return "{\"key\":\"value\"}";
|
|
9644
|
+
case "uuid": return "\"00000000-0000-0000-0000-000000000000\"";
|
|
9645
|
+
case "date": return "\"2026-01-31\"";
|
|
9646
|
+
case "timestamp": return "\"2026-01-31T12:00:00Z\"";
|
|
9647
|
+
case "text[]": return "[\"a\",\"b\"]";
|
|
9648
|
+
case "integer[]": return "[1,2,3]";
|
|
9649
|
+
case "number[]": return "[1.5,2.5]";
|
|
9650
|
+
case "boolean[]": return "[true,false]";
|
|
9651
|
+
case "uuid[]": return "[\"00000000-0000-0000-0000-000000000000\"]";
|
|
9652
|
+
default: return "\"example\"";
|
|
9653
|
+
}
|
|
9654
|
+
}
|
|
9655
|
+
/**
|
|
9656
|
+
* Generate the description string for a single collection tool.
|
|
9657
|
+
*/
|
|
9658
|
+
function describeCollectionTool(verb, ctx) {
|
|
9659
|
+
const { collection, mapped } = ctx;
|
|
9660
|
+
const policy = policySentence(ctx);
|
|
9661
|
+
const exampleCol = exampleWritableColumn(mapped);
|
|
9662
|
+
const exampleKind = exampleCol ? mapped.rowTypeHints[exampleCol] : "text";
|
|
9663
|
+
switch (verb) {
|
|
9664
|
+
case "list": return [
|
|
9665
|
+
`List rows in the "${collection}" collection, newest first.`,
|
|
9666
|
+
`Returns a page of rows plus a \`next_cursor\` — pass it back as \`cursor\` to fetch the next page.`,
|
|
9667
|
+
`Columns: ${allColumnSummary(mapped)}.`,
|
|
9668
|
+
policy,
|
|
9669
|
+
`Example: { "limit": 20 }.`
|
|
9670
|
+
].join(" ");
|
|
9671
|
+
case "get": return [
|
|
9672
|
+
`Fetch one row from "${collection}" by its \`id\`.`,
|
|
9673
|
+
`Returns the full row, or a 404 if no row with that id exists (or it is outside your scope).`,
|
|
9674
|
+
policy,
|
|
9675
|
+
`Example: { "id": "00000000-0000-0000-0000-000000000000" }.`
|
|
9676
|
+
].join(" ");
|
|
9677
|
+
case "find": return [
|
|
9678
|
+
`Find rows in "${collection}" with a typed, per-column filter.`,
|
|
9679
|
+
`Each \`where\` key is a column; its value is a bare value (equality) or an operator object`,
|
|
9680
|
+
`({ eq, ne, gt, gte, lt, lte, in, notIn, like, ilike, isNull, isNotNull, contains, containedBy, overlaps }).`,
|
|
9681
|
+
`Combine with top-level \`and\`/\`or\`/\`not\`. Supports \`order\`, \`limit\`, and \`cursor\` pagination (returns \`next_cursor\`).`,
|
|
9682
|
+
`Columns: ${allColumnSummary(mapped)}.`,
|
|
9683
|
+
policy,
|
|
9684
|
+
exampleCol ? `Example: { "where": { "${exampleCol}": { "eq": ${exampleValue(exampleKind)} } }, "limit": 20 }.` : `Example: { "where": {}, "limit": 20 }.`
|
|
9685
|
+
].join(" ");
|
|
9686
|
+
case "insert": return [
|
|
9687
|
+
`Insert a new row into "${collection}".`,
|
|
9688
|
+
`Provide values for the writable columns: ${writableColumnSummary(mapped)}.`,
|
|
9689
|
+
`Server-managed columns (id, user_id, created_at, updated_at, deleted_at) are stamped automatically — do not send them.`,
|
|
9690
|
+
policy,
|
|
9691
|
+
exampleCol ? `Example: { "${exampleCol}": ${exampleValue(exampleKind)} }.` : `Example: {}.`
|
|
9692
|
+
].join(" ");
|
|
9693
|
+
case "update": return [
|
|
9694
|
+
`Update an existing row in "${collection}" by \`id\` (merge-patch — only the fields you send change).`,
|
|
9695
|
+
`Writable columns: ${writableColumnSummary(mapped)}.`,
|
|
9696
|
+
`Server-managed columns are rejected.`,
|
|
9697
|
+
policy,
|
|
9698
|
+
exampleCol ? `Example: { "id": "…", "set": { "${exampleCol}": ${exampleValue(exampleKind)} } }.` : `Example: { "id": "…", "set": {} }.`
|
|
9699
|
+
].join(" ");
|
|
9700
|
+
case "delete": return [
|
|
9701
|
+
`Delete a row from "${collection}" by \`id\`.`,
|
|
9702
|
+
`Soft-delete by default (recoverable); pass \`hard: true\` for a permanent removal.`,
|
|
9703
|
+
policy,
|
|
9704
|
+
`Example: { "id": "00000000-0000-0000-0000-000000000000" }.`
|
|
9705
|
+
].join(" ");
|
|
9706
|
+
case "aggregate": return [
|
|
9707
|
+
`Compute aggregates over "${collection}": count / sum / avg / min / max, optionally grouped by columns.`,
|
|
9708
|
+
`\`count\` with no column = COUNT(*). Combine with a typed \`where\` to aggregate a subset.`,
|
|
9709
|
+
`Columns: ${allColumnSummary(mapped)}.`,
|
|
9710
|
+
policy,
|
|
9711
|
+
`Example: { "select": [{ "fn": "count" }] }.`
|
|
9712
|
+
].join(" ");
|
|
9713
|
+
case "find_nearest": return [
|
|
9714
|
+
`Vector similarity search over "${collection}" — returns the rows whose embedding is closest to your query vector.`,
|
|
9715
|
+
`This tool exists because "${collection}" has a vector column (${mapped.vectorColumns.join(", ")}).`,
|
|
9716
|
+
policy,
|
|
9717
|
+
`Example: { "vector": [0.1, 0.2, …], "limit": 5 }.`
|
|
9718
|
+
].join(" ");
|
|
9719
|
+
default: return `Operate on the "${collection}" collection.`;
|
|
9720
|
+
}
|
|
9721
|
+
}
|
|
9722
|
+
/** Description for the `<ns>_list_collections` meta tool. */
|
|
9723
|
+
function describeListCollections(namespace) {
|
|
9724
|
+
return [
|
|
9725
|
+
`List every collection exposed by this app's agent surface (namespace "${namespace}").`,
|
|
9726
|
+
`Each collection projects typed list/get/find/insert/update/delete/aggregate tools you can call directly —`,
|
|
9727
|
+
`this tool is the index. Returns each collection's name and column summary.`
|
|
9728
|
+
].join(" ");
|
|
9729
|
+
}
|
|
9730
|
+
/** Description for the `<ns>_whoami` meta tool. */
|
|
9731
|
+
function describeWhoami(namespace) {
|
|
9732
|
+
return [`Report which app project this agent surface is connected to and the scope of the current credential`, `(namespace "${namespace}"). Use it to confirm you are pointed at the right app before mutating data.`].join(" ");
|
|
9733
|
+
}
|
|
9734
|
+
//#endregion
|
|
9735
|
+
//#region src/auto/collection-tools.ts
|
|
9736
|
+
/** Admin-route fetch (mirrors tools/collections.ts adminFetch). */
|
|
9737
|
+
async function adminFetch(apiClient, bearer, options) {
|
|
9738
|
+
let url = `${apiClient.getBaseUrl()}${options.path}`;
|
|
9739
|
+
if (options.query && Object.keys(options.query).length > 0) url += `?${new URLSearchParams(options.query).toString()}`;
|
|
9740
|
+
const headers = {
|
|
9741
|
+
Authorization: `Bearer ${bearer}`,
|
|
9742
|
+
Accept: "application/json"
|
|
9743
|
+
};
|
|
9744
|
+
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
9745
|
+
const res = await fetch(url, {
|
|
9746
|
+
method: options.method,
|
|
9747
|
+
headers,
|
|
9748
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
9749
|
+
});
|
|
9750
|
+
let body;
|
|
9751
|
+
try {
|
|
9752
|
+
body = await res.json();
|
|
9753
|
+
} catch {
|
|
9754
|
+
body = { error: {
|
|
9755
|
+
code: "INVALID_RESPONSE",
|
|
9756
|
+
message: res.statusText
|
|
9757
|
+
} };
|
|
9758
|
+
}
|
|
9759
|
+
return {
|
|
9760
|
+
status: res.status,
|
|
9761
|
+
body
|
|
9762
|
+
};
|
|
9763
|
+
}
|
|
9764
|
+
/**
|
|
9765
|
+
* Tool-name fragment regex. MCP + `tool-naming.test.ts` require lowercase
|
|
9766
|
+
* `_`-separated names. The namespace + collection are sanitized to that
|
|
9767
|
+
* shape so a slug like "My App" or a collection like "MyRecipes" never
|
|
9768
|
+
* produces an invalid tool name.
|
|
9769
|
+
*/
|
|
9770
|
+
function sanitizeNameFragment(raw) {
|
|
9771
|
+
const cleaned = raw.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
9772
|
+
return /^[a-z]/.test(cleaned) ? cleaned : `c_${cleaned}`;
|
|
9773
|
+
}
|
|
9774
|
+
/** Build the `<ns>_<coll>_<verb>` tool name. */
|
|
9775
|
+
function collectionToolName(namespace, collection, verb) {
|
|
9776
|
+
return `${sanitizeNameFragment(namespace)}_${sanitizeNameFragment(collection)}_${verb}`;
|
|
9777
|
+
}
|
|
9778
|
+
/**
|
|
9779
|
+
* Register the typed CRUD tools for a single collection on `server`,
|
|
9780
|
+
* proxying to the existing admin routes via `apiClient`.
|
|
9781
|
+
*/
|
|
9782
|
+
function registerCollectionToolsFor(server, apiClient, options) {
|
|
9783
|
+
const { projectId, namespace, collection, schema, policy, scope } = options;
|
|
9784
|
+
const mapped = mapCollectionSchema(schema, { includeUserIdInInsert: scope === "admin" });
|
|
9785
|
+
const describeCtx = {
|
|
9786
|
+
collection,
|
|
9787
|
+
mapped,
|
|
9788
|
+
readPolicy: policy.read_policy,
|
|
9789
|
+
writePolicy: policy.write_policy,
|
|
9790
|
+
scope
|
|
9791
|
+
};
|
|
9792
|
+
const rowsPath = `/projects/${projectId}/collections/${encodeURIComponent(collection)}/rows`;
|
|
9793
|
+
const name = (verb) => collectionToolName(namespace, collection, verb);
|
|
9794
|
+
registerTool(server, apiClient, name("list"), describeCollectionTool("list", describeCtx), {
|
|
9795
|
+
limit: z.number().int().positive().optional().describe("Max rows (default 50)."),
|
|
9796
|
+
cursor: z.string().optional().describe("Opaque cursor from a prior page."),
|
|
9797
|
+
order: z.union([z.string(), z.array(z.string())]).optional().describe("Order spec, e.g. \"created_at desc\"."),
|
|
9798
|
+
select: z.array(z.string()).optional().describe("Subset of columns to return."),
|
|
9799
|
+
include_deleted: z.boolean().optional().describe("Include soft-deleted rows. Default false.")
|
|
9800
|
+
}, async ({ limit, cursor, order, select, include_deleted }, { pat }) => {
|
|
9801
|
+
const findQuery = {};
|
|
9802
|
+
if (limit !== void 0) findQuery.limit = limit;
|
|
9803
|
+
if (cursor !== void 0) findQuery.cursor = cursor;
|
|
9804
|
+
if (order !== void 0) findQuery.order = order;
|
|
9805
|
+
if (select !== void 0) findQuery.select = select;
|
|
9806
|
+
if (include_deleted !== void 0) findQuery.includeDeleted = include_deleted;
|
|
9807
|
+
return passthroughResult(await adminFetch(apiClient, pat, {
|
|
9808
|
+
method: "GET",
|
|
9809
|
+
path: rowsPath,
|
|
9810
|
+
query: Object.keys(findQuery).length > 0 ? { query: JSON.stringify(findQuery) } : void 0
|
|
9811
|
+
}));
|
|
9812
|
+
});
|
|
9813
|
+
registerTool(server, apiClient, name("get"), describeCollectionTool("get", describeCtx), { id: z.string().describe("Row id (uuid).") }, async ({ id }, { pat }) => {
|
|
9814
|
+
return passthroughResult(await adminFetch(apiClient, pat, {
|
|
9815
|
+
method: "GET",
|
|
9816
|
+
path: `${rowsPath}/${encodeURIComponent(id)}`
|
|
9817
|
+
}));
|
|
9818
|
+
});
|
|
9819
|
+
registerTool(server, apiClient, name("find"), describeCollectionTool("find", describeCtx), {
|
|
9820
|
+
where: z.object({
|
|
9821
|
+
...mapped.whereShape,
|
|
9822
|
+
and: z.array(z.unknown()).optional().describe("AND-combine nested where clauses."),
|
|
9823
|
+
or: z.array(z.unknown()).optional().describe("OR-combine nested where clauses."),
|
|
9824
|
+
not: z.unknown().optional().describe("Negate a nested where clause.")
|
|
9825
|
+
}).partial().optional().describe("Typed, per-column filter."),
|
|
9826
|
+
order: z.union([z.string(), z.array(z.string())]).optional().describe("Order spec, e.g. \"created_at desc\"."),
|
|
9827
|
+
limit: z.number().int().positive().optional().describe("Max rows (default 50)."),
|
|
9828
|
+
cursor: z.string().optional().describe("Opaque cursor from a prior page."),
|
|
9829
|
+
select: z.array(z.string()).optional().describe("Subset of columns to return."),
|
|
9830
|
+
include_deleted: z.boolean().optional().describe("Include soft-deleted rows. Default false.")
|
|
9831
|
+
}, async ({ where, order, limit, cursor, select, include_deleted }, { pat }) => {
|
|
9832
|
+
const findQuery = {};
|
|
9833
|
+
if (where !== void 0) findQuery.where = where;
|
|
9834
|
+
if (order !== void 0) findQuery.order = order;
|
|
9835
|
+
if (limit !== void 0) findQuery.limit = limit;
|
|
9836
|
+
if (cursor !== void 0) findQuery.cursor = cursor;
|
|
9837
|
+
if (select !== void 0) findQuery.select = select;
|
|
9838
|
+
if (include_deleted !== void 0) findQuery.includeDeleted = include_deleted;
|
|
9839
|
+
return passthroughResult(await adminFetch(apiClient, pat, {
|
|
9840
|
+
method: "GET",
|
|
9841
|
+
path: rowsPath,
|
|
9842
|
+
query: Object.keys(findQuery).length > 0 ? { query: JSON.stringify(findQuery) } : void 0
|
|
9843
|
+
}));
|
|
9844
|
+
});
|
|
9845
|
+
registerTool(server, apiClient, name("insert"), describeCollectionTool("insert", describeCtx), {
|
|
9846
|
+
...mapped.insertShape,
|
|
9847
|
+
...scope === "admin" ? { as_system: z.boolean().optional().describe("Admin only. On a shared collection, insert a global row with no owner (user_id NULL). Mutually exclusive with user_id.") } : {}
|
|
9848
|
+
}, async (args, { pat }) => {
|
|
9849
|
+
const row = {};
|
|
9850
|
+
for (const [k, v] of Object.entries(args)) {
|
|
9851
|
+
if (k === "pat") continue;
|
|
9852
|
+
if (v !== void 0) row[k] = v;
|
|
9853
|
+
}
|
|
9854
|
+
return passthroughResult(await adminFetch(apiClient, pat, {
|
|
9855
|
+
method: "POST",
|
|
9856
|
+
path: rowsPath,
|
|
9857
|
+
body: row
|
|
9858
|
+
}));
|
|
9859
|
+
});
|
|
9860
|
+
registerTool(server, apiClient, name("update"), describeCollectionTool("update", describeCtx), {
|
|
9861
|
+
id: z.string().describe("Row id (uuid) to update."),
|
|
9862
|
+
set: z.object({ ...mapped.updateShape }).describe("Column→value map of the fields to change (merge-patch).")
|
|
9863
|
+
}, async ({ id, set }, { pat }) => {
|
|
9864
|
+
return passthroughResult(await adminFetch(apiClient, pat, {
|
|
9865
|
+
method: "PATCH",
|
|
9866
|
+
path: `${rowsPath}/${encodeURIComponent(id)}`,
|
|
9867
|
+
body: { set }
|
|
9868
|
+
}));
|
|
9869
|
+
});
|
|
9870
|
+
registerTool(server, apiClient, name("delete"), describeCollectionTool("delete", describeCtx), {
|
|
9871
|
+
id: z.string().describe("Row id (uuid) to delete."),
|
|
9872
|
+
hard: z.boolean().optional().describe("false (default) soft-deletes (recoverable); true permanently removes.")
|
|
9873
|
+
}, async ({ id, hard }, { pat }) => {
|
|
9874
|
+
return passthroughResult(await adminFetch(apiClient, pat, {
|
|
9875
|
+
method: "DELETE",
|
|
9876
|
+
path: `${rowsPath}/${encodeURIComponent(id)}`,
|
|
9877
|
+
query: hard === true ? { hard: "true" } : void 0
|
|
9878
|
+
}));
|
|
9879
|
+
});
|
|
9880
|
+
registerTool(server, apiClient, name("aggregate"), describeCollectionTool("aggregate", describeCtx), {
|
|
9881
|
+
select: z.array(z.object({
|
|
9882
|
+
fn: z.enum([
|
|
9883
|
+
"count",
|
|
9884
|
+
"sum",
|
|
9885
|
+
"avg",
|
|
9886
|
+
"min",
|
|
9887
|
+
"max"
|
|
9888
|
+
]),
|
|
9889
|
+
column: z.string().optional().describe("Column to aggregate. Required for sum/avg/min/max; omit for count(*)."),
|
|
9890
|
+
as: z.string().optional().describe("Output alias.")
|
|
9891
|
+
})).min(1).describe("One or more aggregations to compute."),
|
|
9892
|
+
group_by: z.union([z.string(), z.array(z.string())]).optional().describe("Column(s) to group by. Omit for a whole-collection aggregate."),
|
|
9893
|
+
where: z.object({ ...mapped.whereShape }).partial().optional().describe("Typed, per-column filter to aggregate a subset."),
|
|
9894
|
+
include_deleted: z.boolean().optional().describe("Include soft-deleted rows. Default false.")
|
|
9895
|
+
}, async ({ select, group_by, where, include_deleted }, { pat }) => {
|
|
9896
|
+
const body = { select };
|
|
9897
|
+
if (group_by !== void 0) body.group_by = group_by;
|
|
9898
|
+
if (where !== void 0) body.where = where;
|
|
9899
|
+
if (include_deleted !== void 0) body.includeDeleted = include_deleted;
|
|
9900
|
+
return passthroughResult(await adminFetch(apiClient, pat, {
|
|
9901
|
+
method: "POST",
|
|
9902
|
+
path: `${rowsPath}/aggregate`,
|
|
9903
|
+
body
|
|
9904
|
+
}));
|
|
9905
|
+
});
|
|
9906
|
+
}
|
|
9907
|
+
//#endregion
|
|
9908
|
+
//#region src/auto/index.ts
|
|
9909
|
+
/** Page size for the admin collections list (the route caps at 200). */
|
|
9910
|
+
const COLLECTION_PAGE_SIZE = 200;
|
|
9911
|
+
/**
|
|
9912
|
+
* Hard cap on total pages fetched, so a bad `total` (or a route that never
|
|
9913
|
+
* advances) can't spin forever. 200 * 100 = 20k collections — far beyond
|
|
9914
|
+
* any real project.
|
|
9915
|
+
*/
|
|
9916
|
+
const COLLECTION_MAX_PAGES = 100;
|
|
9917
|
+
/**
|
|
9918
|
+
* Fetch EVERY collection name for a project, advancing `offset` until the
|
|
9919
|
+
* reported `pagination.total` is reached (the admin list route caps each
|
|
9920
|
+
* page at 200).
|
|
9921
|
+
*
|
|
9922
|
+
* Returns `{ ok, names }`. `ok` is `false` if ANY page (first OR a later
|
|
9923
|
+
* one) returns a non-success status — a partial list must not masquerade
|
|
9924
|
+
* as complete, or the caller would silently register only some of the
|
|
9925
|
+
* project's collections as tools. On a mid-pagination failure the
|
|
9926
|
+
* already-fetched `names` are still returned alongside `ok:false` so a
|
|
9927
|
+
* caller can surface a diagnostic that includes what it did see.
|
|
9928
|
+
*/
|
|
9929
|
+
async function listAllCollections(apiClient, projectId) {
|
|
9930
|
+
const names = [];
|
|
9931
|
+
let offset = 0;
|
|
9932
|
+
for (let page = 0; page < COLLECTION_MAX_PAGES; page++) {
|
|
9933
|
+
const res = await adminGet(apiClient, `/projects/${projectId}/collections?limit=${COLLECTION_PAGE_SIZE}&offset=${offset}`);
|
|
9934
|
+
if (res.status < 200 || res.status >= 300) return {
|
|
9935
|
+
ok: false,
|
|
9936
|
+
names
|
|
9937
|
+
};
|
|
9938
|
+
const pageNames = (res.body.data ?? []).map((c) => c.name).filter((n) => typeof n === "string" && n.length > 0);
|
|
9939
|
+
names.push(...pageNames);
|
|
9940
|
+
const total = res.body.pagination?.total;
|
|
9941
|
+
offset += COLLECTION_PAGE_SIZE;
|
|
9942
|
+
if (typeof total === "number") {
|
|
9943
|
+
if (offset >= total) break;
|
|
9944
|
+
} else if (pageNames.length < COLLECTION_PAGE_SIZE) break;
|
|
9945
|
+
}
|
|
9946
|
+
return {
|
|
9947
|
+
ok: true,
|
|
9948
|
+
names
|
|
9949
|
+
};
|
|
9950
|
+
}
|
|
9951
|
+
/** Admin GET helper bound to a Bearer (no pat-arg machinery needed here). */
|
|
9952
|
+
async function adminGet(apiClient, path) {
|
|
9953
|
+
const res = await fetch(`${apiClient.getBaseUrl()}${path}`, {
|
|
9954
|
+
method: "GET",
|
|
9955
|
+
headers: {
|
|
9956
|
+
Authorization: `Bearer ${await apiClientToken(apiClient)}`,
|
|
9957
|
+
Accept: "application/json"
|
|
9958
|
+
}
|
|
9959
|
+
});
|
|
9960
|
+
let body;
|
|
9961
|
+
try {
|
|
9962
|
+
body = await res.json();
|
|
9963
|
+
} catch {
|
|
9964
|
+
body = {};
|
|
9965
|
+
}
|
|
9966
|
+
return {
|
|
9967
|
+
status: res.status,
|
|
9968
|
+
body
|
|
9969
|
+
};
|
|
9970
|
+
}
|
|
9971
|
+
async function apiClientToken(apiClient) {
|
|
9972
|
+
return await apiClient.resolveTokenOrNull() ?? "";
|
|
9973
|
+
}
|
|
9974
|
+
/**
|
|
9975
|
+
* Register the auto-MCP tool surface for one project on `server`.
|
|
9976
|
+
*
|
|
9977
|
+
* Lists + introspects the project's collections (admin routes) and
|
|
9978
|
+
* registers a typed tool set per collection, plus the two meta tools.
|
|
9979
|
+
* Returns the names of the collections that were exposed plus `listOk`
|
|
9980
|
+
* (false when the collection listing was incomplete — a paginated page
|
|
9981
|
+
* failed — in which case typed tools are deliberately NOT registered so a
|
|
9982
|
+
* truncated set never masquerades as the whole project).
|
|
9983
|
+
*/
|
|
9984
|
+
async function registerAutoMcpTools(server, apiClient, options) {
|
|
9985
|
+
const { projectId, scope } = options;
|
|
9986
|
+
const namespace = sanitizeNameFragment(options.namespace ?? projectId);
|
|
9987
|
+
registerTool(server, apiClient, `${namespace}_whoami`, describeWhoami(namespace), {}, async (_args, { pat }) => {
|
|
9988
|
+
const me = await adminGet(apiClient.withToken(pat), `/projects/${projectId}`);
|
|
9989
|
+
return jsonResult$1({
|
|
9990
|
+
status: me.status,
|
|
9991
|
+
project_id: projectId,
|
|
9992
|
+
scope,
|
|
9993
|
+
namespace,
|
|
9994
|
+
authenticated: me.status >= 200 && me.status < 300
|
|
9995
|
+
});
|
|
9996
|
+
});
|
|
9997
|
+
let collections = [];
|
|
9998
|
+
let listOk = false;
|
|
9999
|
+
try {
|
|
10000
|
+
const all = await listAllCollections(apiClient, projectId);
|
|
10001
|
+
listOk = all.ok;
|
|
10002
|
+
collections = all.ok ? all.names : [];
|
|
10003
|
+
} catch {
|
|
10004
|
+
collections = [];
|
|
10005
|
+
listOk = false;
|
|
10006
|
+
}
|
|
10007
|
+
registerTool(server, apiClient, `${namespace}_list_collections`, describeListCollections(namespace), {}, async (_args, { pat }) => {
|
|
10008
|
+
const boundClient = apiClient.withToken(pat);
|
|
10009
|
+
const all = await listAllCollections(boundClient, projectId);
|
|
10010
|
+
const names = all.names;
|
|
10011
|
+
const described = await Promise.all(names.map(async (collection) => {
|
|
10012
|
+
let exposed = false;
|
|
10013
|
+
try {
|
|
10014
|
+
const res = await adminGet(boundClient, `/projects/${projectId}/collections/${encodeURIComponent(collection)}`);
|
|
10015
|
+
exposed = res.status >= 200 && res.status < 300 && Array.isArray(res.body.data?.columns);
|
|
10016
|
+
} catch {
|
|
10017
|
+
exposed = false;
|
|
10018
|
+
}
|
|
10019
|
+
return {
|
|
10020
|
+
collection,
|
|
10021
|
+
exposed
|
|
10022
|
+
};
|
|
10023
|
+
}));
|
|
10024
|
+
return jsonResult$1({
|
|
10025
|
+
status: all.ok ? 200 : 502,
|
|
10026
|
+
list_complete: all.ok,
|
|
10027
|
+
namespace,
|
|
10028
|
+
project_id: projectId,
|
|
10029
|
+
scope,
|
|
10030
|
+
collections: described.map(({ collection, exposed }) => ({
|
|
10031
|
+
collection,
|
|
10032
|
+
exposed,
|
|
10033
|
+
tools: exposed ? [
|
|
10034
|
+
"list",
|
|
10035
|
+
"get",
|
|
10036
|
+
"find",
|
|
10037
|
+
"insert",
|
|
10038
|
+
"update",
|
|
10039
|
+
"delete",
|
|
10040
|
+
"aggregate"
|
|
10041
|
+
].map((v) => collectionToolName(namespace, collection, v)) : []
|
|
10042
|
+
}))
|
|
10043
|
+
});
|
|
10044
|
+
});
|
|
10045
|
+
for (const collection of collections) {
|
|
10046
|
+
let described;
|
|
10047
|
+
try {
|
|
10048
|
+
const res = await adminGet(apiClient, `/projects/${projectId}/collections/${encodeURIComponent(collection)}`);
|
|
10049
|
+
if (res.status < 200 || res.status >= 300) continue;
|
|
10050
|
+
described = res.body;
|
|
10051
|
+
} catch {
|
|
10052
|
+
continue;
|
|
10053
|
+
}
|
|
10054
|
+
const data = described.data;
|
|
10055
|
+
if (!data || !Array.isArray(data.columns)) continue;
|
|
10056
|
+
const policy = {
|
|
10057
|
+
read_policy: data.read_policy ?? "owner",
|
|
10058
|
+
write_policy: data.write_policy ?? "owner"
|
|
10059
|
+
};
|
|
10060
|
+
registerCollectionToolsFor(server, apiClient, {
|
|
10061
|
+
projectId,
|
|
10062
|
+
namespace,
|
|
10063
|
+
collection,
|
|
10064
|
+
schema: {
|
|
10065
|
+
name: data.name,
|
|
10066
|
+
columns: data.columns
|
|
10067
|
+
},
|
|
10068
|
+
policy,
|
|
10069
|
+
scope
|
|
10070
|
+
});
|
|
10071
|
+
}
|
|
10072
|
+
return {
|
|
10073
|
+
namespace,
|
|
10074
|
+
collections,
|
|
10075
|
+
listOk
|
|
10076
|
+
};
|
|
10077
|
+
}
|
|
10078
|
+
//#endregion
|
|
8667
10079
|
//#region src/index.ts
|
|
8668
10080
|
/**
|
|
8669
10081
|
* Register every Amba MCP tool group against the given server, using the
|
|
@@ -8671,7 +10083,9 @@ function getToolCategory(toolName) {
|
|
|
8671
10083
|
* called against it once.
|
|
8672
10084
|
*/
|
|
8673
10085
|
function registerAllTools(server, apiClient) {
|
|
8674
|
-
registerTools$
|
|
10086
|
+
registerTools$15(server, apiClient);
|
|
10087
|
+
registerTools$37(server, apiClient);
|
|
10088
|
+
registerTools$36(server, apiClient);
|
|
8675
10089
|
registerTools$35(server, apiClient);
|
|
8676
10090
|
registerTools$34(server, apiClient);
|
|
8677
10091
|
registerTools$33(server, apiClient);
|
|
@@ -8692,7 +10106,7 @@ function registerAllTools(server, apiClient) {
|
|
|
8692
10106
|
registerTools$18(server, apiClient);
|
|
8693
10107
|
registerTools$17(server, apiClient);
|
|
8694
10108
|
registerTools$16(server, apiClient);
|
|
8695
|
-
registerTools$
|
|
10109
|
+
registerTools$14(server, apiClient);
|
|
8696
10110
|
registerTools$13(server, apiClient);
|
|
8697
10111
|
registerTools$12(server, apiClient);
|
|
8698
10112
|
registerTools$11(server, apiClient);
|
|
@@ -8727,4 +10141,4 @@ function createApiClient(options) {
|
|
|
8727
10141
|
return new ApiClient(opts);
|
|
8728
10142
|
}
|
|
8729
10143
|
//#endregion
|
|
8730
|
-
export { AMBA_SETUP_ECONOMY_MD, AMBA_SETUP_ECONOMY_MIME, AMBA_SETUP_ECONOMY_URI, AMBA_SETUP_ENGAGEMENT_MD, AMBA_SETUP_ENGAGEMENT_MIME, AMBA_SETUP_ENGAGEMENT_URI, AMBA_SETUP_GAMIFICATION_MD, AMBA_SETUP_GAMIFICATION_MIME, AMBA_SETUP_GAMIFICATION_URI, AMBA_SETUP_GUIDE_BODY_VERSION, AMBA_SETUP_GUIDE_MD, AMBA_SETUP_GUIDE_MIME, AMBA_SETUP_GUIDE_URI, AMBA_SETUP_IDENTITY_MD, AMBA_SETUP_IDENTITY_MIME, AMBA_SETUP_IDENTITY_URI, AMBA_SETUP_INFRASTRUCTURE_MD, AMBA_SETUP_INFRASTRUCTURE_MIME, AMBA_SETUP_INFRASTRUCTURE_URI, AMBA_SETUP_SOCIAL_MD, AMBA_SETUP_SOCIAL_MIME, AMBA_SETUP_SOCIAL_URI, AMBA_SETUP_SUB_RESOURCES, ApiClient, CATEGORY_META, CATEGORY_ORDER, EXPO_BUILD_PROMPT_MD, EXPO_BUILD_PROMPT_MIME, EXPO_BUILD_PROMPT_URI, TOOL_CATEGORY, createApiClient, getToolCategory, registerAllResources, registerAllTools };
|
|
10144
|
+
export { AMBA_SETUP_ECONOMY_MD, AMBA_SETUP_ECONOMY_MIME, AMBA_SETUP_ECONOMY_URI, AMBA_SETUP_ENGAGEMENT_MD, AMBA_SETUP_ENGAGEMENT_MIME, AMBA_SETUP_ENGAGEMENT_URI, AMBA_SETUP_GAMIFICATION_MD, AMBA_SETUP_GAMIFICATION_MIME, AMBA_SETUP_GAMIFICATION_URI, AMBA_SETUP_GUIDE_BODY_VERSION, AMBA_SETUP_GUIDE_MD, AMBA_SETUP_GUIDE_MIME, AMBA_SETUP_GUIDE_URI, AMBA_SETUP_IDENTITY_MD, AMBA_SETUP_IDENTITY_MIME, AMBA_SETUP_IDENTITY_URI, AMBA_SETUP_INFRASTRUCTURE_MD, AMBA_SETUP_INFRASTRUCTURE_MIME, AMBA_SETUP_INFRASTRUCTURE_URI, AMBA_SETUP_SOCIAL_MD, AMBA_SETUP_SOCIAL_MIME, AMBA_SETUP_SOCIAL_URI, AMBA_SETUP_SUB_RESOURCES, ApiClient, CATEGORY_META, CATEGORY_ORDER, EXPO_BUILD_PROMPT_MD, EXPO_BUILD_PROMPT_MIME, EXPO_BUILD_PROMPT_URI, SERVER_MANAGED_COLUMNS, TOOL_CATEGORY, classifyColumn, collectionToolName, createApiClient, describeCollectionTool, describeListCollections, describeWhoami, getToolCategory, mapCollectionSchema, registerAllResources, registerAllTools, registerAutoMcpTools, registerCollectionToolsFor, sanitizeNameFragment };
|