@layers/amba-mcp 4.0.5 → 4.0.7
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 +85 -0
- package/dist/auto/describe.d.ts +57 -0
- package/dist/auto/function-tools.d.ts +51 -0
- package/dist/auto/index.d.ts +100 -0
- package/dist/auto/json-schema-to-zod.d.ts +37 -0
- package/dist/auto/schema-to-zod.d.ts +87 -0
- package/dist/expo-build-prompt.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2750 -181
- package/dist/resources/amba-setup-infrastructure.d.ts +5 -4
- package/dist/resources/amba-setup.d.ts +1 -1
- package/dist/resources/expo-build-prompt.d.ts +1 -1
- package/dist/resources/index.d.ts +1 -1
- package/dist/tools/affiliate.d.ts +12 -0
- package/dist/tools/agent-checkout.d.ts +17 -0
- package/dist/tools/ai-prompts-admin.d.ts +3 -2
- package/dist/tools/app-mcp.d.ts +22 -0
- package/dist/tools/domains.d.ts +2 -0
- package/dist/tools/monetization.d.ts +20 -0
- package/dist/tools/orgs.d.ts +8 -0
- package/dist/tools/payments.d.ts +31 -0
- package/dist/tools/promotion.d.ts +5 -1
- package/dist/tools/secrets.d.ts +3 -4
- package/dist/tools/service-accounts.d.ts +10 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { INTEGRATION_PROVIDERS, integrationConfigSummary } from "@layers/amba-shared";
|
|
6
|
+
import { INTEGRATION_PROVIDERS, getBindingReservationReason, integrationConfigSummary } from "@layers/amba-shared";
|
|
7
7
|
//#region src/auth.ts
|
|
8
8
|
const CREDENTIALS_PATH = join(homedir(), ".amba", "credentials.json");
|
|
9
9
|
async function loadCredentials() {
|
|
@@ -310,7 +310,14 @@ 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",
|
|
320
|
+
"capabilities"
|
|
314
321
|
]);
|
|
315
322
|
/**
|
|
316
323
|
* Destructive verbs. A tool whose name contains one of these MAY perform
|
|
@@ -323,7 +330,8 @@ const DESTRUCTIVE_VERBS = new Set([
|
|
|
323
330
|
"remove",
|
|
324
331
|
"revoke",
|
|
325
332
|
"drop",
|
|
326
|
-
"reset"
|
|
333
|
+
"reset",
|
|
334
|
+
"ban"
|
|
327
335
|
]);
|
|
328
336
|
/**
|
|
329
337
|
* Write verbs — mutations that are additive / non-destructive (create a
|
|
@@ -345,6 +353,7 @@ const WRITE_VERBS = new Set([
|
|
|
345
353
|
"deploy",
|
|
346
354
|
"send",
|
|
347
355
|
"add",
|
|
356
|
+
"attach",
|
|
348
357
|
"configure",
|
|
349
358
|
"invoke",
|
|
350
359
|
"schedule",
|
|
@@ -352,6 +361,8 @@ const WRITE_VERBS = new Set([
|
|
|
352
361
|
"upload",
|
|
353
362
|
"rotate",
|
|
354
363
|
"reprovision",
|
|
364
|
+
"provision",
|
|
365
|
+
"payout",
|
|
355
366
|
"transfer",
|
|
356
367
|
"pause",
|
|
357
368
|
"resume",
|
|
@@ -367,11 +378,23 @@ const WRITE_VERBS = new Set([
|
|
|
367
378
|
"insert",
|
|
368
379
|
"invite",
|
|
369
380
|
"register",
|
|
381
|
+
"define",
|
|
382
|
+
"map",
|
|
383
|
+
"adopt",
|
|
384
|
+
"apply",
|
|
370
385
|
"purchase",
|
|
371
386
|
"signup",
|
|
372
387
|
"login",
|
|
373
388
|
"refresh",
|
|
374
|
-
"track"
|
|
389
|
+
"track",
|
|
390
|
+
"charge",
|
|
391
|
+
"refund",
|
|
392
|
+
"enable",
|
|
393
|
+
"accept",
|
|
394
|
+
"generate",
|
|
395
|
+
"enroll",
|
|
396
|
+
"request",
|
|
397
|
+
"clawback"
|
|
375
398
|
]);
|
|
376
399
|
/**
|
|
377
400
|
* Split a tool name into lowercase verb-candidate tokens, dropping the
|
|
@@ -433,7 +456,10 @@ const NOUN_LIKE_VERBS = new Set([
|
|
|
433
456
|
"tiers",
|
|
434
457
|
"analytics",
|
|
435
458
|
"schedule",
|
|
436
|
-
"me"
|
|
459
|
+
"me",
|
|
460
|
+
"balance",
|
|
461
|
+
"payouts",
|
|
462
|
+
"capabilities"
|
|
437
463
|
]);
|
|
438
464
|
/**
|
|
439
465
|
* The verbs eligible to be a title's leading word: every classification
|
|
@@ -624,15 +650,15 @@ function registerPublicTool(server, name, description, schema, handler, aliases
|
|
|
624
650
|
}
|
|
625
651
|
//#endregion
|
|
626
652
|
//#region src/tools/projects.ts
|
|
627
|
-
function registerTools$
|
|
628
|
-
registerTool(server, apiClient, "amba_projects_list", "List all Amba projects owned by the authenticated developer. Returns project id, name, bundle_id, platform, and
|
|
653
|
+
function registerTools$42(server, apiClient) {
|
|
654
|
+
registerTool(server, apiClient, "amba_projects_list", "List all Amba projects owned by the authenticated developer. Returns project id, name, bundle_id, platform, environment, the persisted billing `tier`, and `effective_tier` (the runtime entitlement tier; comped projects are effectively enterprise).", {}, async (_, { client }) => {
|
|
629
655
|
const result = await client.get("/projects");
|
|
630
656
|
return { content: [{
|
|
631
657
|
type: "text",
|
|
632
658
|
text: JSON.stringify(result, null, 2)
|
|
633
659
|
}] };
|
|
634
660
|
}, ["amba_list_projects"]);
|
|
635
|
-
registerTool(server, apiClient, "amba_projects_get", "Get detailed information about a specific Amba project by its ID, including API keys, integrations, and
|
|
661
|
+
registerTool(server, apiClient, "amba_projects_get", "Get detailed information about a specific Amba project by its ID, including API keys, integrations, configuration, persisted billing `tier`, and runtime `effective_tier`. Use `effective_tier` for quota/feature decisions; `tier` remains the stored subscription classification.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
636
662
|
const result = await client.get(`/projects/${project_id}`);
|
|
637
663
|
return { content: [{
|
|
638
664
|
type: "text",
|
|
@@ -731,7 +757,7 @@ function registerTools$35(server, apiClient) {
|
|
|
731
757
|
}
|
|
732
758
|
//#endregion
|
|
733
759
|
//#region src/tools/push.ts
|
|
734
|
-
function registerTools$
|
|
760
|
+
function registerTools$41(server, apiClient) {
|
|
735
761
|
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
762
|
project_id: z.string().describe("The project ID"),
|
|
737
763
|
title: z.string().describe("Push notification title shown to the user"),
|
|
@@ -861,7 +887,7 @@ const segmentRulesSchema = z.object({
|
|
|
861
887
|
operator: z.enum(["AND", "OR"]).describe("Logical operator combining conditions"),
|
|
862
888
|
conditions: z.array(segmentConditionSchema).describe("Array of filter conditions")
|
|
863
889
|
});
|
|
864
|
-
function registerTools$
|
|
890
|
+
function registerTools$40(server, apiClient) {
|
|
865
891
|
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
892
|
project_id: z.string().describe("The project ID"),
|
|
867
893
|
include_system: z.boolean().optional().describe("Include built-in system segments in the result. Defaults to false.")
|
|
@@ -946,7 +972,7 @@ const configConditionSchema = z.object({
|
|
|
946
972
|
percentage: z.number().optional().describe("Percentage rollout (0-100)"),
|
|
947
973
|
value: z.unknown().describe("Override value for this condition")
|
|
948
974
|
});
|
|
949
|
-
function registerTools$
|
|
975
|
+
function registerTools$39(server, apiClient) {
|
|
950
976
|
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
977
|
project_id: z.string().describe("The project ID"),
|
|
952
978
|
include_system: z.boolean().optional().describe("Include platform-provided default config keys in the result. Defaults to false.")
|
|
@@ -1024,7 +1050,7 @@ const contentItemSchema = z.object({
|
|
|
1024
1050
|
metadata: z.record(z.unknown()).optional().describe("Arbitrary metadata key-value pairs"),
|
|
1025
1051
|
is_premium: z.boolean().optional().describe("Whether this content requires a premium entitlement")
|
|
1026
1052
|
});
|
|
1027
|
-
function registerTools$
|
|
1053
|
+
function registerTools$38(server, apiClient) {
|
|
1028
1054
|
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
1055
|
project_id: z.string().describe("The project ID"),
|
|
1030
1056
|
name: z.string().describe("Library name (e.g. \"Daily Motivation\", \"Workout Tips\")"),
|
|
@@ -1189,6 +1215,45 @@ function registerTools$31(server, apiClient) {
|
|
|
1189
1215
|
text: JSON.stringify(result, null, 2)
|
|
1190
1216
|
}] };
|
|
1191
1217
|
});
|
|
1218
|
+
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.", {
|
|
1219
|
+
project_id: z.string().describe("The project ID"),
|
|
1220
|
+
item_id: z.string().describe("The content item ID to translate"),
|
|
1221
|
+
language: z.string().describe("BCP-47 language tag for this translation (e.g. \"en\", \"es-419\", \"pt-BR\")"),
|
|
1222
|
+
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."),
|
|
1223
|
+
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."),
|
|
1224
|
+
metadata: z.record(z.unknown()).optional().describe("Optional per-language metadata (e.g. locale-specific media URL)")
|
|
1225
|
+
}, async ({ project_id, item_id, language, title, body, metadata }, { client }) => {
|
|
1226
|
+
const payload = {};
|
|
1227
|
+
if (title !== void 0) payload.title = title;
|
|
1228
|
+
if (body !== void 0) payload.body = body;
|
|
1229
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
1230
|
+
const result = await client.put(`/projects/${project_id}/content/items/${item_id}/translations/${encodeURIComponent(language)}`, payload);
|
|
1231
|
+
return { content: [{
|
|
1232
|
+
type: "text",
|
|
1233
|
+
text: JSON.stringify(result, null, 2)
|
|
1234
|
+
}] };
|
|
1235
|
+
});
|
|
1236
|
+
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.", {
|
|
1237
|
+
project_id: z.string().describe("The project ID"),
|
|
1238
|
+
item_id: z.string().describe("The content item ID")
|
|
1239
|
+
}, async ({ project_id, item_id }, { client }) => {
|
|
1240
|
+
const result = await client.get(`/projects/${project_id}/content/items/${item_id}/translations`);
|
|
1241
|
+
return { content: [{
|
|
1242
|
+
type: "text",
|
|
1243
|
+
text: JSON.stringify(result, null, 2)
|
|
1244
|
+
}] };
|
|
1245
|
+
});
|
|
1246
|
+
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.", {
|
|
1247
|
+
project_id: z.string().describe("The project ID"),
|
|
1248
|
+
item_id: z.string().describe("The content item ID"),
|
|
1249
|
+
language: z.string().describe("BCP-47 language tag of the translation to delete")
|
|
1250
|
+
}, async ({ project_id, item_id, language }, { client }) => {
|
|
1251
|
+
const result = await client.delete(`/projects/${project_id}/content/items/${item_id}/translations/${encodeURIComponent(language)}`);
|
|
1252
|
+
return { content: [{
|
|
1253
|
+
type: "text",
|
|
1254
|
+
text: JSON.stringify(result, null, 2)
|
|
1255
|
+
}] };
|
|
1256
|
+
});
|
|
1192
1257
|
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
1258
|
project_id: z.string().describe("The project ID"),
|
|
1194
1259
|
library_id: z.string().describe("The content library ID"),
|
|
@@ -1203,7 +1268,7 @@ function registerTools$31(server, apiClient) {
|
|
|
1203
1268
|
}
|
|
1204
1269
|
//#endregion
|
|
1205
1270
|
//#region src/tools/streaks.ts
|
|
1206
|
-
function registerTools$
|
|
1271
|
+
function registerTools$37(server, apiClient) {
|
|
1207
1272
|
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
1273
|
project_id: z.string().describe("The project ID"),
|
|
1209
1274
|
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,8 +1360,8 @@ function registerTools$30(server, apiClient) {
|
|
|
1295
1360
|
//#endregion
|
|
1296
1361
|
//#region src/tools/integrations.ts
|
|
1297
1362
|
const providerEnum = z.enum(INTEGRATION_PROVIDERS);
|
|
1298
|
-
function registerTools$
|
|
1299
|
-
registerTool(server, apiClient, "amba_integrations_configure", "Configure (create or replace) a third-party integration for a project. Supported providers: \"apns\" (Apple Push), \"fcm\" (Firebase Cloud Messaging), \"revenuecat\" (subscription management), \"superwall\" (paywall management). Each provider requires specific config fields. Push credentials (.p8 / service-account JSON) are passed inline here and stored server-side automatically — no separate secrets step.", {
|
|
1363
|
+
function registerTools$36(server, apiClient) {
|
|
1364
|
+
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), \"stripe_billing\" (web subscriptions through the app's own Stripe Billing account — web purchases grant the same Amba entitlements as mobile ones; the response includes the webhook_url to register in the Stripe dashboard), \"google_play\" (a Play Console service account with the Android Publisher permission — lets `amba_monetization_apply` create one-time Play products directly, price included). 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
1365
|
project_id: z.string().describe("The project ID"),
|
|
1301
1366
|
provider: providerEnum.describe("Integration provider name"),
|
|
1302
1367
|
config: z.record(z.unknown()).describe("Provider-specific configuration. " + integrationConfigSummary())
|
|
@@ -1365,7 +1430,7 @@ const PERIOD_DAYS = {
|
|
|
1365
1430
|
"30d": "30",
|
|
1366
1431
|
"90d": "90"
|
|
1367
1432
|
};
|
|
1368
|
-
function registerTools$
|
|
1433
|
+
function registerTools$35(server, apiClient) {
|
|
1369
1434
|
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
1435
|
project_id: z.string().describe("The project ID"),
|
|
1371
1436
|
period: z.enum([
|
|
@@ -1438,7 +1503,7 @@ function passthroughResult(result) {
|
|
|
1438
1503
|
}
|
|
1439
1504
|
//#endregion
|
|
1440
1505
|
//#region src/tools/users.ts
|
|
1441
|
-
function registerTools$
|
|
1506
|
+
function registerTools$34(server, apiClient) {
|
|
1442
1507
|
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
1508
|
project_id: z.string().describe("The project ID"),
|
|
1444
1509
|
limit: z.number().optional().describe("Maximum number of users to return (default 50)"),
|
|
@@ -1885,7 +1950,7 @@ final dailyLimit = await Amba.config.get<int>('daily_limit', defaultValue: 10);
|
|
|
1885
1950
|
final streak = await Amba.streaks.get('daily_login');
|
|
1886
1951
|
print('Current streak: \${streak.currentCount}');`
|
|
1887
1952
|
};
|
|
1888
|
-
function registerTools$
|
|
1953
|
+
function registerTools$33(server, apiClient) {
|
|
1889
1954
|
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
1955
|
"ios",
|
|
1891
1956
|
"android",
|
|
@@ -1970,7 +2035,7 @@ const criteriaSchema = z.object({
|
|
|
1970
2035
|
property_key: z.string().optional().describe("User property key (required for property_value type)"),
|
|
1971
2036
|
target_value: z.number().describe("Target value to unlock the achievement")
|
|
1972
2037
|
});
|
|
1973
|
-
function registerTools$
|
|
2038
|
+
function registerTools$32(server, apiClient) {
|
|
1974
2039
|
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
2040
|
project_id: z.string().describe("The project ID"),
|
|
1976
2041
|
key: z.string().describe("Unique key for this achievement (e.g. \"first_workout\", \"streak_master_7\")"),
|
|
@@ -2075,7 +2140,7 @@ function registerTools$25(server, apiClient) {
|
|
|
2075
2140
|
}
|
|
2076
2141
|
//#endregion
|
|
2077
2142
|
//#region src/tools/challenges.ts
|
|
2078
|
-
function registerTools$
|
|
2143
|
+
function registerTools$31(server, apiClient) {
|
|
2079
2144
|
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
2145
|
project_id: z.string().describe("The project ID"),
|
|
2081
2146
|
name: z.string().describe("Challenge name (e.g. \"7-Day Fitness Sprint\", \"XP Weekend Blitz\")"),
|
|
@@ -2186,7 +2251,7 @@ function registerTools$24(server, apiClient) {
|
|
|
2186
2251
|
}
|
|
2187
2252
|
//#endregion
|
|
2188
2253
|
//#region src/tools/economy.ts
|
|
2189
|
-
function registerTools$
|
|
2254
|
+
function registerTools$30(server, apiClient) {
|
|
2190
2255
|
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
2256
|
project_id: z.string().describe("The project ID"),
|
|
2192
2257
|
code: z.string().describe("Unique currency code (e.g. \"gold\", \"gems\", \"hearts\")"),
|
|
@@ -2443,12 +2508,12 @@ function registerTools$23(server, apiClient) {
|
|
|
2443
2508
|
app_user_id: z.string().describe("The app_user UUID to grant the entitlement to"),
|
|
2444
2509
|
entitlement_id: z.string().describe("Entitlement identifier (e.g. \"premium\", \"pro_annual\"). Required."),
|
|
2445
2510
|
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("
|
|
2511
|
+
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
2512
|
store: z.enum([
|
|
2448
2513
|
"app_store",
|
|
2449
2514
|
"play_store",
|
|
2450
|
-
"
|
|
2451
|
-
]).nullable().optional().describe("Originating store. Pass null to clear."),
|
|
2515
|
+
"web"
|
|
2516
|
+
]).nullable().optional().describe("Originating store (\"web\" covers any non-native checkout). Pass null to clear."),
|
|
2452
2517
|
purchase_date: z.string().nullable().optional().describe("ISO-8601 purchase timestamp. Pass null to clear."),
|
|
2453
2518
|
expiration_date: z.string().nullable().optional().describe("ISO-8601 expiration timestamp. Pass null to clear (e.g. for a lifetime grant)."),
|
|
2454
2519
|
period_type: z.enum([
|
|
@@ -2472,6 +2537,99 @@ function registerTools$23(server, apiClient) {
|
|
|
2472
2537
|
text: JSON.stringify(result, null, 2)
|
|
2473
2538
|
}] };
|
|
2474
2539
|
});
|
|
2540
|
+
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(" "), {
|
|
2541
|
+
project_id: z.string().describe("The project ID"),
|
|
2542
|
+
key: z.string().describe("Stable entitlement key (e.g. \"pro\", \"premium_content\"). Required."),
|
|
2543
|
+
display_name: z.string().optional().describe("Human-readable name for dashboards."),
|
|
2544
|
+
description: z.string().optional().describe("What this entitlement unlocks."),
|
|
2545
|
+
metadata: z.record(z.unknown()).optional().describe("Opaque metadata object.")
|
|
2546
|
+
}, async ({ project_id, key, display_name, description, metadata }, { client }) => {
|
|
2547
|
+
const payload = { key };
|
|
2548
|
+
if (display_name !== void 0) payload.display_name = display_name;
|
|
2549
|
+
if (description !== void 0) payload.description = description;
|
|
2550
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
2551
|
+
const result = await client.post(`/projects/${project_id}/subscriptions/entitlements`, payload);
|
|
2552
|
+
return { content: [{
|
|
2553
|
+
type: "text",
|
|
2554
|
+
text: JSON.stringify(result, null, 2)
|
|
2555
|
+
}] };
|
|
2556
|
+
});
|
|
2557
|
+
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 lets every purchase path (mobile stores, web billing, server grants) land on the same entitlement set."].join(" "), {
|
|
2558
|
+
project_id: z.string().describe("The project ID"),
|
|
2559
|
+
product_id: z.string().describe("Stable, neutral product key (e.g. \"pro_yearly\"). Required."),
|
|
2560
|
+
grants_entitlement_id: z.string().optional().describe("The entitlement key this product unlocks (must be defined first)."),
|
|
2561
|
+
display_name: z.string().optional().describe("Human-readable product name."),
|
|
2562
|
+
description: z.string().optional().describe("Marketing description for the paywall."),
|
|
2563
|
+
product_type: z.enum([
|
|
2564
|
+
"subscription",
|
|
2565
|
+
"consumable",
|
|
2566
|
+
"non_consumable",
|
|
2567
|
+
"non_renewing"
|
|
2568
|
+
]).optional().describe("Product type (default \"subscription\")."),
|
|
2569
|
+
store_product_refs: z.record(z.string()).optional().describe("Map of store id → that store's product identifier."),
|
|
2570
|
+
trial_period_days: z.number().int().nonnegative().optional().describe("Free-trial length in days (display hint)."),
|
|
2571
|
+
duration: z.string().optional().describe("ISO-8601 duration display hint (e.g. \"P1M\", \"P1Y\")."),
|
|
2572
|
+
metadata: z.record(z.unknown()).optional().describe("Opaque metadata object.")
|
|
2573
|
+
}, async ({ project_id, product_id, grants_entitlement_id, display_name, description, product_type, store_product_refs, trial_period_days, duration, metadata }, { client }) => {
|
|
2574
|
+
const payload = { product_id };
|
|
2575
|
+
if (grants_entitlement_id !== void 0) payload.grants_entitlement_id = grants_entitlement_id;
|
|
2576
|
+
if (display_name !== void 0) payload.display_name = display_name;
|
|
2577
|
+
if (description !== void 0) payload.description = description;
|
|
2578
|
+
if (product_type !== void 0) payload.product_type = product_type;
|
|
2579
|
+
if (store_product_refs !== void 0) payload.store_product_refs = store_product_refs;
|
|
2580
|
+
if (trial_period_days !== void 0) payload.trial_period_days = trial_period_days;
|
|
2581
|
+
if (duration !== void 0) payload.duration = duration;
|
|
2582
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
2583
|
+
const result = await client.post(`/projects/${project_id}/subscriptions/products`, payload);
|
|
2584
|
+
return { content: [{
|
|
2585
|
+
type: "text",
|
|
2586
|
+
text: JSON.stringify(result, null, 2)
|
|
2587
|
+
}] };
|
|
2588
|
+
});
|
|
2589
|
+
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.", {
|
|
2590
|
+
project_id: z.string().describe("The project ID"),
|
|
2591
|
+
product_id: z.string().describe("The declared product id."),
|
|
2592
|
+
entitlement_id: z.string().describe("The declared entitlement key to also grant.")
|
|
2593
|
+
}, async ({ project_id, product_id, entitlement_id }, { client }) => {
|
|
2594
|
+
const result = await client.post(`/projects/${project_id}/subscriptions/products/${product_id}/entitlements`, { entitlement_id });
|
|
2595
|
+
return { content: [{
|
|
2596
|
+
type: "text",
|
|
2597
|
+
text: JSON.stringify(result, null, 2)
|
|
2598
|
+
}] };
|
|
2599
|
+
});
|
|
2600
|
+
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(" "), {
|
|
2601
|
+
project_id: z.string().describe("The project ID"),
|
|
2602
|
+
offering_id: z.string().describe("Stable offering id (e.g. \"default\", \"holiday_2026\")."),
|
|
2603
|
+
display_name: z.string().optional().describe("Human-readable name."),
|
|
2604
|
+
description: z.string().optional().describe("Internal description."),
|
|
2605
|
+
is_current: z.boolean().optional().describe("Make this the default offering the SDK returns (default false)."),
|
|
2606
|
+
packages: z.array(z.object({
|
|
2607
|
+
package_id: z.string().describe("Package id unique within the offering (e.g. \"$annual\")."),
|
|
2608
|
+
product_id: z.string().optional().describe("The product this package surfaces (by product_id)."),
|
|
2609
|
+
position: z.number().int().nonnegative().optional().describe("Display order (lower first)."),
|
|
2610
|
+
metadata: z.record(z.unknown()).optional().describe("Opaque package metadata.")
|
|
2611
|
+
})).optional().describe("The packages in this offering (replaces the existing set)."),
|
|
2612
|
+
metadata: z.record(z.unknown()).optional().describe("Opaque offering metadata.")
|
|
2613
|
+
}, async ({ project_id, offering_id, display_name, description, is_current, packages, metadata }, { client }) => {
|
|
2614
|
+
const payload = { offering_id };
|
|
2615
|
+
if (display_name !== void 0) payload.display_name = display_name;
|
|
2616
|
+
if (description !== void 0) payload.description = description;
|
|
2617
|
+
if (is_current !== void 0) payload.is_current = is_current;
|
|
2618
|
+
if (packages !== void 0) payload.packages = packages;
|
|
2619
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
2620
|
+
const result = await client.post(`/projects/${project_id}/subscriptions/offerings`, payload);
|
|
2621
|
+
return { content: [{
|
|
2622
|
+
type: "text",
|
|
2623
|
+
text: JSON.stringify(result, null, 2)
|
|
2624
|
+
}] };
|
|
2625
|
+
});
|
|
2626
|
+
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 }) => {
|
|
2627
|
+
const result = await client.get(`/projects/${project_id}/subscriptions/offerings`);
|
|
2628
|
+
return { content: [{
|
|
2629
|
+
type: "text",
|
|
2630
|
+
text: JSON.stringify(result, null, 2)
|
|
2631
|
+
}] };
|
|
2632
|
+
});
|
|
2475
2633
|
registerTool(server, apiClient, "amba_users_get_inventory", "View a user's inventory including all owned items and quantities.", {
|
|
2476
2634
|
project_id: z.string().describe("The project ID"),
|
|
2477
2635
|
app_user_id: z.string().describe("The user ID to look up")
|
|
@@ -2776,8 +2934,81 @@ function registerTools$23(server, apiClient) {
|
|
|
2776
2934
|
});
|
|
2777
2935
|
}
|
|
2778
2936
|
//#endregion
|
|
2937
|
+
//#region src/tools/monetization.ts
|
|
2938
|
+
const objectTypeSchema = z.enum([
|
|
2939
|
+
"entitlement",
|
|
2940
|
+
"product",
|
|
2941
|
+
"offering",
|
|
2942
|
+
"package",
|
|
2943
|
+
"paywall"
|
|
2944
|
+
]);
|
|
2945
|
+
function registerTools$29(server, apiClient) {
|
|
2946
|
+
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 — including store-product creation steps for declared products that do not exist upstream yet), a store-floor preflight (blocking gaps: dangling product references, or no provider app connected for a store), a `human_floor` checklist (store-side steps no API can do — store SKUs outside the App Store, pricing, agreements/banking/tax, review — each with its exact console location, ready to relay to the developer), 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 }) => {
|
|
2947
|
+
const result = await client.get(`/projects/${project_id}/monetization/plan`);
|
|
2948
|
+
return { content: [{
|
|
2949
|
+
type: "text",
|
|
2950
|
+
text: JSON.stringify(result, null, 2)
|
|
2951
|
+
}] };
|
|
2952
|
+
});
|
|
2953
|
+
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 }) => {
|
|
2954
|
+
const result = await client.get(`/projects/${project_id}/monetization/drift`);
|
|
2955
|
+
return { content: [{
|
|
2956
|
+
type: "text",
|
|
2957
|
+
text: JSON.stringify(result, null, 2)
|
|
2958
|
+
}] };
|
|
2959
|
+
});
|
|
2960
|
+
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 }) => {
|
|
2961
|
+
const result = await client.get(`/projects/${project_id}/monetization/export`);
|
|
2962
|
+
return { content: [{
|
|
2963
|
+
type: "text",
|
|
2964
|
+
text: JSON.stringify(result, null, 2)
|
|
2965
|
+
}] };
|
|
2966
|
+
});
|
|
2967
|
+
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 }) => {
|
|
2968
|
+
const result = await client.get(`/projects/${project_id}/monetization/definitions`);
|
|
2969
|
+
return { content: [{
|
|
2970
|
+
type: "text",
|
|
2971
|
+
text: JSON.stringify(result, null, 2)
|
|
2972
|
+
}] };
|
|
2973
|
+
});
|
|
2974
|
+
registerTool(server, apiClient, "amba_monetization_apply", "Apply the declared monetization config to the connected provider (RevenueCat). DURABLE + ASYNC: this starts the apply and returns an `operation_id` — poll it with `amba_operations_get` until `succeeded` | `failed` (writes are paced against the provider's rate limits, so large plans take a few minutes; the apply survives restarts and retries safely via per-op idempotency keys). 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/product, remove a package) are GATED behind a confirmation of the exact plan_hash: pass `confirm` = plan_hash up front, or call apply without it to get `confirm_required: true` (the apply then WAITS for confirmation — re-call apply with `confirm` to approve that pending apply; a confirmation that no longer matches the plan is rejected and nothing destructive runs). STORE-PRODUCT steps go further: declared products missing upstream are CREATED through the provider (registered with RevenueCat, and — with store_metadata.push_to_store — created as REAL store products: App Store products in App Store Connect when they declare duration + subscription_group_name; Google Play ONE-TIME products in Google Play directly when they declare play_price_micros + play_currency, with the price set as part of the create — connect a `google_play` integration first), but these store catalog writes NEVER run off an up-front `confirm`: the apply always waits, you review the pending store steps plus the returned `human_floor` checklist (App Store pricing, agreements/banking/tax, review — with exact console locations), then approve by re-calling apply with `confirm`. Setting the current/default offering, publishing a paywall live, store SKUs apply can't create (Play subscriptions + other stores), and App Store pricing are NOT API-driven — they're returned in `refused`/`human_floor` 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 an unresolvable store reference, 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.", {
|
|
2975
|
+
project_id: z.string().describe("The project ID"),
|
|
2976
|
+
plan_hash: z.string().describe("The plan_hash returned by a fresh amba_monetization_plan. Apply refuses if the provider drifted off this hash."),
|
|
2977
|
+
confirm: z.string().optional().describe("Set to the SAME plan_hash to confirm and EXECUTE the gated destructive ops (detach/archive/remove). When a plan has gated ops and confirm is omitted, the apply waits for confirmation (re-call apply with confirm to approve it); plans without gated ops apply without confirmation. Store-product creation steps ALWAYS wait — an up-front confirm never auto-runs a store catalog write; approve the started apply by re-calling with confirm."),
|
|
2978
|
+
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)."),
|
|
2979
|
+
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.")
|
|
2980
|
+
}, async ({ project_id, plan_hash, confirm, allow_detach_live, reconcile }, { client }) => {
|
|
2981
|
+
const body = { plan_hash };
|
|
2982
|
+
if (confirm !== void 0) body["confirm"] = confirm;
|
|
2983
|
+
if (allow_detach_live !== void 0) body["allow_detach_live"] = allow_detach_live;
|
|
2984
|
+
if (reconcile !== void 0) body["reconcile"] = reconcile;
|
|
2985
|
+
const result = await client.post(`/projects/${project_id}/monetization/apply`, body);
|
|
2986
|
+
return { content: [{
|
|
2987
|
+
type: "text",
|
|
2988
|
+
text: JSON.stringify(result, null, 2)
|
|
2989
|
+
}] };
|
|
2990
|
+
});
|
|
2991
|
+
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.", {
|
|
2992
|
+
project_id: z.string().describe("The project ID"),
|
|
2993
|
+
adopt_all: z.boolean().optional().describe("Adopt every live object. Mutually sufficient with `objects`."),
|
|
2994
|
+
objects: z.array(z.object({
|
|
2995
|
+
object_type: objectTypeSchema.describe("The kind of object to adopt"),
|
|
2996
|
+
identifier: z.string().describe("The object stable identifier (provider id)")
|
|
2997
|
+
})).optional().describe("Specific objects to adopt. Provide this OR adopt_all.")
|
|
2998
|
+
}, async ({ project_id, adopt_all, objects }, { client }) => {
|
|
2999
|
+
const body = {};
|
|
3000
|
+
if (adopt_all !== void 0) body["adopt_all"] = adopt_all;
|
|
3001
|
+
if (objects !== void 0) body["objects"] = objects;
|
|
3002
|
+
const result = await client.post(`/projects/${project_id}/monetization/adopt`, body);
|
|
3003
|
+
return { content: [{
|
|
3004
|
+
type: "text",
|
|
3005
|
+
text: JSON.stringify(result, null, 2)
|
|
3006
|
+
}] };
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
//#endregion
|
|
2779
3010
|
//#region src/tools/leaderboards.ts
|
|
2780
|
-
function registerTools$
|
|
3011
|
+
function registerTools$28(server, apiClient) {
|
|
2781
3012
|
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
3013
|
project_id: z.string().describe("The project ID"),
|
|
2783
3014
|
name: z.string().describe("Leaderboard name (e.g. \"Top XP Earners\", \"Weekly Streak Leaders\")"),
|
|
@@ -2892,7 +3123,7 @@ function registerTools$22(server, apiClient) {
|
|
|
2892
3123
|
* rollover (the scheduled weekly rollover) and score live off `xp_awarded`
|
|
2893
3124
|
* engagement events.
|
|
2894
3125
|
*/
|
|
2895
|
-
function registerTools$
|
|
3126
|
+
function registerTools$27(server, apiClient) {
|
|
2896
3127
|
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
3128
|
project_id: z.string().describe("The project ID"),
|
|
2898
3129
|
name: z.string().describe("League tier name, e.g. \"Bronze\", \"Silver\", \"Gold\""),
|
|
@@ -2955,7 +3186,7 @@ function registerTools$21(server, apiClient) {
|
|
|
2955
3186
|
}
|
|
2956
3187
|
//#endregion
|
|
2957
3188
|
//#region src/tools/platform.ts
|
|
2958
|
-
function registerTools$
|
|
3189
|
+
function registerTools$26(server, apiClient) {
|
|
2959
3190
|
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
3191
|
project_id: z.string().describe("The project ID"),
|
|
2961
3192
|
name: z.string().describe("Flow name (e.g. \"Welcome Flow\", \"Premium Onboarding\")"),
|
|
@@ -3230,6 +3461,83 @@ function registerTools$20(server, apiClient) {
|
|
|
3230
3461
|
text: JSON.stringify(result, null, 2)
|
|
3231
3462
|
}] };
|
|
3232
3463
|
});
|
|
3464
|
+
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.", {
|
|
3465
|
+
project_id: z.string().describe("The project ID"),
|
|
3466
|
+
slug: z.string().describe("URL-safe handle your app reads the catalog by (lowercase, digits, hyphens; e.g. \"onboarding-art\"). Unique per project."),
|
|
3467
|
+
name: z.string().describe("Human-readable catalog name (e.g. \"Onboarding Illustrations\")"),
|
|
3468
|
+
description: z.string().optional().describe("Optional description of what the catalog holds"),
|
|
3469
|
+
is_public: z.boolean().optional().describe("Whether end-user apps can read this catalog by slug. Defaults to true."),
|
|
3470
|
+
metadata: z.record(z.unknown()).optional().describe("Custom metadata")
|
|
3471
|
+
}, async ({ project_id, slug, name, description, is_public, metadata }, { client }) => {
|
|
3472
|
+
const payload = {
|
|
3473
|
+
slug,
|
|
3474
|
+
name
|
|
3475
|
+
};
|
|
3476
|
+
if (description !== void 0) payload.description = description;
|
|
3477
|
+
if (is_public !== void 0) payload.is_public = is_public;
|
|
3478
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
3479
|
+
return jsonResult$1(await client.post(`/projects/${project_id}/media/catalogs`, payload));
|
|
3480
|
+
});
|
|
3481
|
+
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.", {
|
|
3482
|
+
project_id: z.string().describe("The project ID"),
|
|
3483
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
3484
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
3485
|
+
}, async ({ project_id, limit, offset }, { client }) => {
|
|
3486
|
+
const query = {};
|
|
3487
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
3488
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
3489
|
+
return jsonResult$1(await client.get(`/projects/${project_id}/media/catalogs`, query));
|
|
3490
|
+
});
|
|
3491
|
+
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.", {
|
|
3492
|
+
project_id: z.string().describe("The project ID"),
|
|
3493
|
+
catalog_id: z.string().describe("The catalog ID")
|
|
3494
|
+
}, async ({ project_id, catalog_id }, { client }) => {
|
|
3495
|
+
return jsonResult$1(await client.get(`/projects/${project_id}/media/catalogs/${catalog_id}`));
|
|
3496
|
+
});
|
|
3497
|
+
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.", {
|
|
3498
|
+
project_id: z.string().describe("The project ID"),
|
|
3499
|
+
catalog_id: z.string().describe("The catalog ID"),
|
|
3500
|
+
slug: z.string().optional().describe("New URL-safe slug (lowercase, digits, hyphens)"),
|
|
3501
|
+
name: z.string().optional().describe("New catalog name"),
|
|
3502
|
+
description: z.string().optional().describe("New description"),
|
|
3503
|
+
is_public: z.boolean().optional().describe("Whether end-user apps can read this catalog"),
|
|
3504
|
+
metadata: z.record(z.unknown()).optional().describe("Replacement metadata object")
|
|
3505
|
+
}, async ({ project_id, catalog_id, slug, name, description, is_public, metadata }, { client }) => {
|
|
3506
|
+
const payload = {};
|
|
3507
|
+
if (slug !== void 0) payload.slug = slug;
|
|
3508
|
+
if (name !== void 0) payload.name = name;
|
|
3509
|
+
if (description !== void 0) payload.description = description;
|
|
3510
|
+
if (is_public !== void 0) payload.is_public = is_public;
|
|
3511
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
3512
|
+
return jsonResult$1(await client.patch(`/projects/${project_id}/media/catalogs/${catalog_id}`, payload));
|
|
3513
|
+
});
|
|
3514
|
+
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).", {
|
|
3515
|
+
project_id: z.string().describe("The project ID"),
|
|
3516
|
+
catalog_id: z.string().describe("The catalog ID")
|
|
3517
|
+
}, async ({ project_id, catalog_id }, { client }) => {
|
|
3518
|
+
return jsonResult$1(await client.delete(`/projects/${project_id}/media/catalogs/${catalog_id}`));
|
|
3519
|
+
});
|
|
3520
|
+
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.", {
|
|
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 add (from amba_media_list)"),
|
|
3524
|
+
position: z.number().optional().describe("Sort position within the catalog (lower comes first). Defaults to 0."),
|
|
3525
|
+
caption: z.string().optional().describe("Optional per-catalog caption for this asset"),
|
|
3526
|
+
metadata: z.record(z.unknown()).optional().describe("Custom metadata for this catalog item")
|
|
3527
|
+
}, async ({ project_id, catalog_id, media_id, position, caption, metadata }, { client }) => {
|
|
3528
|
+
const payload = { media_id };
|
|
3529
|
+
if (position !== void 0) payload.position = position;
|
|
3530
|
+
if (caption !== void 0) payload.caption = caption;
|
|
3531
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
3532
|
+
return jsonResult$1(await client.post(`/projects/${project_id}/media/catalogs/${catalog_id}/items`, payload));
|
|
3533
|
+
});
|
|
3534
|
+
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.", {
|
|
3535
|
+
project_id: z.string().describe("The project ID"),
|
|
3536
|
+
catalog_id: z.string().describe("The catalog ID"),
|
|
3537
|
+
media_id: z.string().describe("ID of the media asset to remove from the catalog")
|
|
3538
|
+
}, async ({ project_id, catalog_id, media_id }, { client }) => {
|
|
3539
|
+
return jsonResult$1(await client.delete(`/projects/${project_id}/media/catalogs/${catalog_id}/items/${media_id}`));
|
|
3540
|
+
});
|
|
3233
3541
|
registerTool(server, apiClient, "amba_moderation_queue_get", "Get the content moderation queue for a project. Shows reported content pending review.", {
|
|
3234
3542
|
project_id: z.string().describe("The project ID"),
|
|
3235
3543
|
status: z.enum([
|
|
@@ -3531,7 +3839,7 @@ function registerTools$20(server, apiClient) {
|
|
|
3531
3839
|
}
|
|
3532
3840
|
//#endregion
|
|
3533
3841
|
//#region src/tools/social.ts
|
|
3534
|
-
function registerTools$
|
|
3842
|
+
function registerTools$25(server, apiClient) {
|
|
3535
3843
|
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
3844
|
const result = await client.get(`/projects/${project_id}/friends/stats`);
|
|
3537
3845
|
return { content: [{
|
|
@@ -3856,7 +4164,7 @@ function registerTools$19(server, apiClient) {
|
|
|
3856
4164
|
}
|
|
3857
4165
|
//#endregion
|
|
3858
4166
|
//#region src/tools/xp.ts
|
|
3859
|
-
function registerTools$
|
|
4167
|
+
function registerTools$24(server, apiClient) {
|
|
3860
4168
|
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
4169
|
project_id: z.string().describe("The project ID"),
|
|
3862
4170
|
name: z.string().describe("Rule name (e.g. \"Workout Completed\", \"Daily Login Bonus\")"),
|
|
@@ -3979,7 +4287,7 @@ function registerTools$18(server, apiClient) {
|
|
|
3979
4287
|
}
|
|
3980
4288
|
//#endregion
|
|
3981
4289
|
//#region src/tools/events.ts
|
|
3982
|
-
function registerTools$
|
|
4290
|
+
function registerTools$23(server, apiClient) {
|
|
3983
4291
|
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
4292
|
project_id: z.string().describe("The project ID"),
|
|
3985
4293
|
since: z.string().optional().describe("ISO-8601 lower bound for occurred_at. Defaults to 24h ago."),
|
|
@@ -4019,6 +4327,22 @@ function registerTools$17(server, apiClient) {
|
|
|
4019
4327
|
text: JSON.stringify(result, null, 2)
|
|
4020
4328
|
}] };
|
|
4021
4329
|
});
|
|
4330
|
+
registerTool(server, apiClient, "amba_events_explain", [
|
|
4331
|
+
"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.",
|
|
4332
|
+
"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.",
|
|
4333
|
+
"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.",
|
|
4334
|
+
"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."
|
|
4335
|
+
].join(" "), {
|
|
4336
|
+
project_id: z.string().describe("The project ID"),
|
|
4337
|
+
event_name: z.string().describe("Candidate event name to evaluate (e.g. \"lesson_completed\")."),
|
|
4338
|
+
properties: z.record(z.unknown()).optional().describe("Candidate event properties (e.g. {amount: 5}). Read by property-scaled grant rules."),
|
|
4339
|
+
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.")
|
|
4340
|
+
}, async ({ project_id, event_name, properties, user_id }, { client }) => {
|
|
4341
|
+
const event = { name: event_name };
|
|
4342
|
+
if (properties !== void 0) event.properties = properties;
|
|
4343
|
+
if (user_id !== void 0) event.user_id = user_id;
|
|
4344
|
+
return jsonResult$1(await client.post(`/projects/${project_id}/events/explain`, { event }));
|
|
4345
|
+
});
|
|
4022
4346
|
registerTool(server, apiClient, "amba_events_track", [
|
|
4023
4347
|
"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
4348
|
"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 +4378,7 @@ function registerTools$17(server, apiClient) {
|
|
|
4054
4378
|
}
|
|
4055
4379
|
//#endregion
|
|
4056
4380
|
//#region src/tools/event-catalog.ts
|
|
4057
|
-
function registerTools$
|
|
4381
|
+
function registerTools$22(server, apiClient) {
|
|
4058
4382
|
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
4383
|
project_id: z.string().describe("The project ID"),
|
|
4060
4384
|
plane: z.enum(["app", "control"]).optional().describe("Filter to one plane. Omit for both.")
|
|
@@ -4124,7 +4448,7 @@ const funnelStepSchema = z.object({
|
|
|
4124
4448
|
event: z.string().describe("Event name to match (engagement event_name)."),
|
|
4125
4449
|
filters: z.array(funnelFilterSchema).optional().describe("Optional property predicates, ANDed together, that an event must satisfy.")
|
|
4126
4450
|
});
|
|
4127
|
-
function registerTools$
|
|
4451
|
+
function registerTools$21(server, apiClient) {
|
|
4128
4452
|
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
4453
|
const result = await client.get(`/projects/${project_id}/funnels`);
|
|
4130
4454
|
return { content: [{
|
|
@@ -4340,7 +4664,7 @@ function enrichedAuthResult(result, agentInstructions) {
|
|
|
4340
4664
|
status: result.status
|
|
4341
4665
|
});
|
|
4342
4666
|
}
|
|
4343
|
-
function registerTools$
|
|
4667
|
+
function registerTools$20(server, apiClient) {
|
|
4344
4668
|
registerPublicTool(server, "amba_developer_signup", [
|
|
4345
4669
|
"Create a new Amba developer account. Returns a long-lived Personal Access Token (PAT)",
|
|
4346
4670
|
"plus a real isolated Amba project (provisioning asynchronously), plus ready-to-paste",
|
|
@@ -4358,7 +4682,34 @@ function registerTools$14(server, apiClient) {
|
|
|
4358
4682
|
].join(" "), {
|
|
4359
4683
|
email: z.string().email().describe("Developer email address. Will be lowercased."),
|
|
4360
4684
|
password: z.string().min(8).describe("Password, minimum 8 characters."),
|
|
4361
|
-
name: z.string().optional().describe("Optional display name.")
|
|
4685
|
+
name: z.string().optional().describe("Optional display name."),
|
|
4686
|
+
referral_code: z.string().optional().describe("Optional Amba partner referral code the developer arrived with (the `ref` on a partner's link). Attributes a later paid upgrade to that partner; ignored if not a live partner code.")
|
|
4687
|
+
}, async ({ email, password, name, referral_code }) => {
|
|
4688
|
+
const body = {
|
|
4689
|
+
email,
|
|
4690
|
+
password
|
|
4691
|
+
};
|
|
4692
|
+
if (name !== void 0) body.name = name;
|
|
4693
|
+
if (referral_code !== void 0) body.referral_code = referral_code;
|
|
4694
|
+
return enrichedAuthResult(await authFetch(apiClient, {
|
|
4695
|
+
method: "POST",
|
|
4696
|
+
path: "/auth/developer/signup",
|
|
4697
|
+
body
|
|
4698
|
+
}), SIGNUP_AGENT_INSTRUCTIONS);
|
|
4699
|
+
});
|
|
4700
|
+
registerPublicTool(server, "amba_affiliate_signup", [
|
|
4701
|
+
"Join an affiliate program from zero: create an Amba account + org and get a long-lived PAT",
|
|
4702
|
+
"plus ready-to-paste MCP-client config in `mcp_config`. An affiliate IS an Amba org — after",
|
|
4703
|
+
"signing up, give your org id (from amba_orgs_list) to the program owner so they can enroll you",
|
|
4704
|
+
"(amba_affiliate_invite with affiliate_org_id), then set up payouts with",
|
|
4705
|
+
"amba_affiliate_my_payout_account_create. Agent: pass the returned `pat` inline on subsequent calls",
|
|
4706
|
+
"and write the mcp_config snippet to the customer's MCP config for future sessions.",
|
|
4707
|
+
"Email is lowercased; password must be at least 8 characters."
|
|
4708
|
+
].join(" "), {
|
|
4709
|
+
email: z.string().email().describe("Affiliate email address. Will be lowercased."),
|
|
4710
|
+
password: z.string().min(8).describe("Password, minimum 8 characters."),
|
|
4711
|
+
name: z.string().optional().describe("Optional display name."),
|
|
4712
|
+
invite_token: z.string().optional().describe("Optional affiliate invite token (for reference).")
|
|
4362
4713
|
}, async ({ email, password, name }) => {
|
|
4363
4714
|
const body = {
|
|
4364
4715
|
email,
|
|
@@ -4447,7 +4798,7 @@ function registerTools$14(server, apiClient) {
|
|
|
4447
4798
|
}
|
|
4448
4799
|
//#endregion
|
|
4449
4800
|
//#region src/tools/collections.ts
|
|
4450
|
-
async function adminFetch(apiClient, options) {
|
|
4801
|
+
async function adminFetch$1(apiClient, options) {
|
|
4451
4802
|
let url = `${apiClient.getBaseUrl()}${options.path}`;
|
|
4452
4803
|
if (options.query && Object.keys(options.query).length > 0) url += `?${new URLSearchParams(options.query).toString()}`;
|
|
4453
4804
|
const headers = {
|
|
@@ -4455,6 +4806,7 @@ async function adminFetch(apiClient, options) {
|
|
|
4455
4806
|
Accept: "application/json"
|
|
4456
4807
|
};
|
|
4457
4808
|
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
4809
|
+
if (options.idempotencyKey !== void 0) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
4458
4810
|
const res = await fetch(url, {
|
|
4459
4811
|
method: options.method,
|
|
4460
4812
|
headers,
|
|
@@ -4486,7 +4838,7 @@ function catalogShapeWarning(row) {
|
|
|
4486
4838
|
const ownership = "user_id" in row ? `attributed to user_id "${String(row["user_id"])}"` : "attributed to the seeding user (or rejected if the collection requires user_id)";
|
|
4487
4839
|
return [`⚠️ This row has catalog-shaped column(s) [${hit.join(", ")}] but is being inserted into a per-user collection, so it will be ${ownership} — other users will NOT see it via the client SDK.`, "If this is shared/global content (a catalog, articles, a media library), use a Content Library (amba_content_libraries_create) or create the collection with shared:true so its rows are readable by every user. Proceeding anyway."].join(" ");
|
|
4488
4840
|
}
|
|
4489
|
-
async function clientFetch(apiClient, options) {
|
|
4841
|
+
async function clientFetch$1(apiClient, options) {
|
|
4490
4842
|
let url = `${apiClient.getApiRoot()}/client${options.path}`;
|
|
4491
4843
|
if (options.query && Object.keys(options.query).length > 0) url += `?${new URLSearchParams(options.query).toString()}`;
|
|
4492
4844
|
const headers = {
|
|
@@ -4495,6 +4847,7 @@ async function clientFetch(apiClient, options) {
|
|
|
4495
4847
|
};
|
|
4496
4848
|
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
4497
4849
|
if (options.sessionToken) headers["Authorization"] = `Bearer ${options.sessionToken}`;
|
|
4850
|
+
if (options.idempotencyKey !== void 0) headers["Idempotency-Key"] = options.idempotencyKey;
|
|
4498
4851
|
const res = await fetch(url, {
|
|
4499
4852
|
method: options.method,
|
|
4500
4853
|
headers,
|
|
@@ -4521,26 +4874,70 @@ const MISSING_API_KEY_ERROR = jsonResult$1({
|
|
|
4521
4874
|
message: "Client tools require `api_key` (the project client X-Api-Key). Mint one via amba_api_keys_create with key_type=\"client\"."
|
|
4522
4875
|
}
|
|
4523
4876
|
});
|
|
4877
|
+
const COLLECTION_COLUMN_TYPES = [
|
|
4878
|
+
"uuid",
|
|
4879
|
+
"text",
|
|
4880
|
+
"integer",
|
|
4881
|
+
"bigint",
|
|
4882
|
+
"numeric",
|
|
4883
|
+
"boolean",
|
|
4884
|
+
"timestamptz",
|
|
4885
|
+
"date",
|
|
4886
|
+
"jsonb",
|
|
4887
|
+
"vector",
|
|
4888
|
+
"text[]",
|
|
4889
|
+
"integer[]",
|
|
4890
|
+
"bigint[]",
|
|
4891
|
+
"numeric[]",
|
|
4892
|
+
"boolean[]",
|
|
4893
|
+
"uuid[]"
|
|
4894
|
+
];
|
|
4895
|
+
const CHANGE_TYPE_TARGET_TYPES = [
|
|
4896
|
+
"uuid",
|
|
4897
|
+
"text",
|
|
4898
|
+
"integer",
|
|
4899
|
+
"bigint",
|
|
4900
|
+
"numeric",
|
|
4901
|
+
"boolean",
|
|
4902
|
+
"timestamptz",
|
|
4903
|
+
"date",
|
|
4904
|
+
"jsonb",
|
|
4905
|
+
"text[]",
|
|
4906
|
+
"integer[]",
|
|
4907
|
+
"bigint[]",
|
|
4908
|
+
"numeric[]",
|
|
4909
|
+
"boolean[]",
|
|
4910
|
+
"uuid[]"
|
|
4911
|
+
];
|
|
4524
4912
|
const columnSchema = z.object({
|
|
4525
|
-
name: z.string().describe("Column name. Must match /^[a-z][a-z0-9_]*$/."),
|
|
4526
|
-
type: z.
|
|
4527
|
-
nullable: z.boolean().optional().describe("Whether the column accepts NULL. Defaults to true."),
|
|
4528
|
-
|
|
4529
|
-
|
|
4913
|
+
name: z.string().describe("Column name. Must match /^[a-z][a-z0-9_]*$/. Server-managed names (id, user_id, created_at, updated_at, deleted_at) and the query keywords and/or/not are reserved."),
|
|
4914
|
+
type: z.enum(COLLECTION_COLUMN_TYPES).describe("Column type. Scalars: text, integer, bigint, numeric, boolean, timestamptz, date, jsonb, uuid. Arrays: text[], integer[], bigint[], numeric[], boolean[], uuid[] — array columns unlock the contains/contained_by/overlaps query operators. `vector` requires `dimension`. Column defaults are not supported — set values at insert time."),
|
|
4915
|
+
nullable: z.boolean().optional().describe("Whether the column accepts NULL. Defaults to false — columns are NOT NULL unless nullable is true."),
|
|
4916
|
+
dimension: z.number().int().min(1).max(4096).optional().describe("Required for `vector` columns: the embedding dimension (1–4096), e.g. 1536 for OpenAI text-embedding-3-small. Rejected on non-vector columns."),
|
|
4917
|
+
references: z.object({
|
|
4918
|
+
table: z.string().describe("Target table, e.g. app_users."),
|
|
4919
|
+
column: z.string().optional().describe("Target column. Defaults to id."),
|
|
4920
|
+
onDelete: z.enum([
|
|
4921
|
+
"CASCADE",
|
|
4922
|
+
"RESTRICT",
|
|
4923
|
+
"SET NULL",
|
|
4924
|
+
"NO ACTION"
|
|
4925
|
+
]).optional().describe("Cascade behavior. Defaults to RESTRICT.")
|
|
4926
|
+
}).optional().describe("Optional foreign key. Not allowed on vector columns. (Every collection already gets a user_id FK to app_users automatically — no need to declare it.)")
|
|
4530
4927
|
});
|
|
4531
4928
|
const indexSchema = z.object({
|
|
4532
4929
|
name: z.string().optional().describe("Index name. Defaults to a generated value."),
|
|
4533
|
-
columns: z.array(z.string()).describe("Column
|
|
4534
|
-
unique: z.boolean().optional().describe("Whether the index enforces uniqueness."),
|
|
4535
|
-
|
|
4536
|
-
"
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
"
|
|
4542
|
-
"ivfflat"
|
|
4543
|
-
|
|
4930
|
+
columns: z.array(z.string()).min(1).describe("Column entries — each is \"<col>\" or \"<col> desc\" (ascending is the default). For a vector index, exactly one entry naming the vector column."),
|
|
4931
|
+
unique: z.boolean().optional().describe("Whether the index enforces uniqueness. Not supported on vector indexes."),
|
|
4932
|
+
vector: z.object({
|
|
4933
|
+
method: z.enum(["ivfflat", "hnsw"]).optional().describe("Index method. Default ivfflat (fast build); hnsw trades slower writes for better recall on read-heavy workloads."),
|
|
4934
|
+
distance: z.enum([
|
|
4935
|
+
"cosine",
|
|
4936
|
+
"l2",
|
|
4937
|
+
"inner_product"
|
|
4938
|
+
]).optional().describe("Distance operator the index optimizes for. Default cosine. Must match the `distance` used by find-nearest queries against the column, or the index is skipped."),
|
|
4939
|
+
lists: z.number().int().min(1).max(1e5).optional().describe("ivfflat only: number of inverted lists. Default 100; sqrt(row count) is a good rule of thumb.")
|
|
4940
|
+
}).optional().describe("Vector index spec — set it (even as {}) to build a similarity index on a vector column for find-nearest. Omit for a standard B-tree index; non-vector indexes never need it.")
|
|
4544
4941
|
});
|
|
4545
4942
|
const sdkFilterSchema = z.unknown().describe("SDK-shaped filter. Either a leaf `{column, op, value}` (op ∈ eq|ne|gt|gte|lt|lte|in|not_in|like|ilike|is_null|is_not_null) or a combinator `{and: [...]}` / `{or: [...]}` / `{not: {...}}`.");
|
|
4546
4943
|
const sdkOrderSchema = z.array(z.object({
|
|
@@ -4549,9 +4946,10 @@ const sdkOrderSchema = z.array(z.object({
|
|
|
4549
4946
|
})).describe("Ordering. `[{column: \"created_at\", direction: \"desc\"}, ...]`.");
|
|
4550
4947
|
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
4948
|
const setSchema = z.record(z.unknown()).describe("Column-value map. Server-managed columns (id, created_at, etc.) are rejected.");
|
|
4552
|
-
function registerTools$
|
|
4949
|
+
function registerTools$19(server, apiClient) {
|
|
4553
4950
|
registerTool(server, apiClient, "amba_collections_create", [
|
|
4554
4951
|
"Create a new collection (schema-first Postgres table) in a project.",
|
|
4952
|
+
"Ownership: by default every row is private to the user who created it. For team/org data, pass owner_scope:'group' with read_policy:'member' and write_policy:'member:admin' — rows are owned by a group and any member reads while admins write, enforced server-side via the group's membership so you don't reimplement authorization in app code.",
|
|
4555
4953
|
"The schema change is applied server-side via an async workflow.",
|
|
4556
4954
|
"Authenticates as the developer/agent — pass `pat` (the Personal Access Token returned by amba_developer_signup) or send it as the inbound Bearer.",
|
|
4557
4955
|
"Returns the workflow id + version + status. Failure responses include `details.workflow_id` for debugging.",
|
|
@@ -4561,15 +4959,21 @@ function registerTools$13(server, apiClient) {
|
|
|
4561
4959
|
name: z.string().describe("Collection name. Must match /^[a-z][a-z0-9_]*$/. Reserved prefixes (amba_, coll_amba_) are rejected."),
|
|
4562
4960
|
columns: z.array(columnSchema).describe("Column definitions. Server adds id/user_id/created_at/updated_at/deleted_at."),
|
|
4563
4961
|
indexes: z.array(indexSchema).optional().describe("Optional extra indexes (server creates a primary key + user_id index by default)."),
|
|
4564
|
-
shared: z.boolean().optional().describe("Set true for developer-seeded GLOBAL content not owned by any single user (question banks, content libraries, lookup tables). Makes user_id NULLABLE so rows can be inserted without a user_id. Defaults to false (every row requires a user_id).")
|
|
4565
|
-
|
|
4962
|
+
shared: z.boolean().optional().describe("Set true for developer-seeded GLOBAL content not owned by any single user (question banks, content libraries, lookup tables). Makes user_id NULLABLE so rows can be inserted without a user_id. Defaults to false (every row requires a user_id)."),
|
|
4963
|
+
owner_scope: z.enum(["user", "group"]).optional().describe("Row ownership model. 'user' (default) — each row is private to the user who created it (profiles, drafts, per-user state). 'group' — rows are owned by a team (a group_id) and shared with its members; pair with read_policy:'member' / write_policy:'member:admin'. Reach for 'group' the moment two users must read or write the same rows (team workspaces, org-shared data) — Amba resolves membership server-side so you can delete your app-side authorization layer."),
|
|
4964
|
+
read_policy: z.string().optional().describe("Who can READ rows — Postgres-grade row security, declared once and enforced on every query. 'owner' (default, private), 'public' (every signed-in user), 'member' (any member of the owning group; requires owner_scope:'group'), 'member:<role>' (members at role >= <role>, where owner>admin>member>viewer)."),
|
|
4965
|
+
write_policy: z.string().optional().describe("Who can WRITE rows. 'owner' (default), 'authenticated' (any signed-in user may insert), 'member' (any member of the owning group), 'member:admin' (the classic 'members read, admins write'; requires owner_scope:'group'). Reads and writes are independent.")
|
|
4966
|
+
}, async ({ project_id, name, columns, indexes, shared, owner_scope, read_policy, write_policy }, { pat }) => {
|
|
4566
4967
|
const body = {
|
|
4567
4968
|
name,
|
|
4568
4969
|
columns
|
|
4569
4970
|
};
|
|
4570
4971
|
if (indexes !== void 0) body.indexes = indexes;
|
|
4571
4972
|
if (shared !== void 0) body.shared = shared;
|
|
4572
|
-
|
|
4973
|
+
if (owner_scope !== void 0) body.owner_scope = owner_scope;
|
|
4974
|
+
if (read_policy !== void 0) body.read_policy = read_policy;
|
|
4975
|
+
if (write_policy !== void 0) body.write_policy = write_policy;
|
|
4976
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4573
4977
|
method: "POST",
|
|
4574
4978
|
path: `/projects/${project_id}/collections`,
|
|
4575
4979
|
body,
|
|
@@ -4588,7 +4992,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4588
4992
|
const query = {};
|
|
4589
4993
|
if (limit !== void 0) query.limit = String(limit);
|
|
4590
4994
|
if (offset !== void 0) query.offset = String(offset);
|
|
4591
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
4995
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4592
4996
|
method: "GET",
|
|
4593
4997
|
path: `/projects/${project_id}/collections`,
|
|
4594
4998
|
query,
|
|
@@ -4603,7 +5007,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4603
5007
|
project_id: z.string().describe("The project ID."),
|
|
4604
5008
|
name: z.string().describe("Collection name (customer-facing, no `coll_` prefix).")
|
|
4605
5009
|
}, async ({ project_id, name }, { pat }) => {
|
|
4606
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5010
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4607
5011
|
method: "GET",
|
|
4608
5012
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}`,
|
|
4609
5013
|
bearer: pat
|
|
@@ -4618,7 +5022,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4618
5022
|
name: z.string().describe("Collection name to drop."),
|
|
4619
5023
|
confirm: z.string().describe("Must equal the collection name. Accident guard for destructive drops.")
|
|
4620
5024
|
}, async ({ project_id, name, confirm }, { pat }) => {
|
|
4621
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5025
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4622
5026
|
method: "DELETE",
|
|
4623
5027
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}`,
|
|
4624
5028
|
query: { confirm },
|
|
@@ -4637,7 +5041,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4637
5041
|
}, async ({ project_id, name, hard }, { pat }) => {
|
|
4638
5042
|
const query = {};
|
|
4639
5043
|
if (hard === false) query.hard = "false";
|
|
4640
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5044
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4641
5045
|
method: "DELETE",
|
|
4642
5046
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/data`,
|
|
4643
5047
|
query,
|
|
@@ -4645,7 +5049,8 @@ function registerTools$13(server, apiClient) {
|
|
|
4645
5049
|
}));
|
|
4646
5050
|
});
|
|
4647
5051
|
registerTool(server, apiClient, "amba_collections_alter", [
|
|
4648
|
-
"Alter a collection: exactly one of `add_column`, `add_index`, `change_type`, `rename_column`, `drop_column`, or `
|
|
5052
|
+
"Alter a collection: exactly one of `add_column`, `add_index`, `change_type`, `rename_column`, `drop_column`, `relax_user_id`, or `set_policy` per call (the underlying saga is built around one SQL blob per workflow; set_policy is a metadata-only write applied without the saga).",
|
|
5053
|
+
"set_policy changes an existing collection's access policy: read_policy ('owner'|'public'|'member'|'member:<role>') and/or write_policy ('owner'|'authenticated'|'member'|'member:admin'). A 'member' policy requires the collection to have been created with owner_scope:'group' (owner scope is fixed at create time).",
|
|
4649
5054
|
"Drop-column is destructive — requires `confirm` to equal the dropped column name.",
|
|
4650
5055
|
"change_type changes an existing column to a new type. It REWRITES THE WHOLE TABLE under an exclusive lock — the collection is briefly unreadable/unwritable while it runs, so prefer low-traffic windows for large collections. Casts that are not trivially safe (e.g. text→integer) require a `using` cast expression; known-safe casts (anything→text, integer→bigint, integer/bigint→numeric) do not. Without a `using` for an unsafe cast you get a 400 up front rather than a mid-migration failure.",
|
|
4651
5056
|
"rename_column renames a column (metadata-only, fast). It BREAKS any app code referencing the old name — re-run `amba types generate` and update call sites afterward. The new name can’t collide with a server-managed column (id, user_id, created_at, updated_at, deleted_at) or a reserved query keyword (and, or, not).",
|
|
@@ -4659,8 +5064,8 @@ function registerTools$13(server, apiClient) {
|
|
|
4659
5064
|
add_index: indexSchema.optional().describe("Index to add. Mutually exclusive with the other alter operations."),
|
|
4660
5065
|
change_type: z.object({
|
|
4661
5066
|
column: z.string().describe("Existing column to change."),
|
|
4662
|
-
to_type: z.
|
|
4663
|
-
from_type: z.
|
|
5067
|
+
to_type: z.enum(CHANGE_TYPE_TARGET_TYPES).describe("Target type — any column type except vector (text, integer, bigint, numeric, boolean, timestamptz, date, jsonb, uuid, or an array type like text[]). To move a column to vector, add a new vector column instead."),
|
|
5068
|
+
from_type: z.enum(COLLECTION_COLUMN_TYPES).optional().describe("Optional current type. When provided, known-safe widening casts (integer→bigint, integer/bigint→numeric) skip the `using` requirement."),
|
|
4664
5069
|
using: z.string().optional().describe("SQL cast expression for the USING clause, e.g. \"score::integer\". Required for casts that are not trivially safe; omit only for anything→text or a known-safe widening cast.")
|
|
4665
5070
|
}).optional().describe("Change a column’s type. REWRITES THE WHOLE TABLE under an exclusive lock (briefly unreadable/unwritable). Mutually exclusive with the other alter operations."),
|
|
4666
5071
|
rename_column: z.object({
|
|
@@ -4669,8 +5074,12 @@ function registerTools$13(server, apiClient) {
|
|
|
4669
5074
|
}).optional().describe("Rename a column (metadata-only, fast). BREAKS code referencing the old name — re-run `amba types generate` afterward. Mutually exclusive with the other alter operations."),
|
|
4670
5075
|
drop_column: z.string().optional().describe("Column name to drop. Requires `confirm` query param to match."),
|
|
4671
5076
|
relax_user_id: z.boolean().optional().describe("Set true to drop the user_id NOT NULL constraint so this collection can hold shared/global rows (NULL user_id). Mutually exclusive with the other operations."),
|
|
5077
|
+
set_policy: z.object({
|
|
5078
|
+
read_policy: z.string().optional(),
|
|
5079
|
+
write_policy: z.string().optional()
|
|
5080
|
+
}).optional().describe("Change the access policy (metadata-only). read_policy: 'owner'|'public'|'member'|'member:<role>'; write_policy: 'owner'|'authenticated'|'member'|'member:admin'. 'member' policies require a group-owned collection (created with owner_scope:'group'). Mutually exclusive with the other operations."),
|
|
4672
5081
|
confirm: z.string().optional().describe("Required when drop_column is set; must equal the column name.")
|
|
4673
|
-
}, async ({ project_id, name, add_column, add_index, change_type, rename_column, drop_column, relax_user_id, confirm }, { pat }) => {
|
|
5082
|
+
}, async ({ project_id, name, add_column, add_index, change_type, rename_column, drop_column, relax_user_id, set_policy, confirm }, { pat }) => {
|
|
4674
5083
|
const body = {};
|
|
4675
5084
|
if (add_column !== void 0) body.add_column = add_column;
|
|
4676
5085
|
if (add_index !== void 0) body.add_index = add_index;
|
|
@@ -4678,9 +5087,10 @@ function registerTools$13(server, apiClient) {
|
|
|
4678
5087
|
if (rename_column !== void 0) body.rename_column = rename_column;
|
|
4679
5088
|
if (drop_column !== void 0) body.drop_column = drop_column;
|
|
4680
5089
|
if (relax_user_id !== void 0) body.relax_user_id = relax_user_id;
|
|
5090
|
+
if (set_policy !== void 0) body.set_policy = set_policy;
|
|
4681
5091
|
const query = {};
|
|
4682
5092
|
if (drop_column !== void 0 && confirm !== void 0) query.confirm = confirm;
|
|
4683
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5093
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4684
5094
|
method: "PATCH",
|
|
4685
5095
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}`,
|
|
4686
5096
|
body,
|
|
@@ -4699,6 +5109,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4699
5109
|
project_id: z.string().describe("The project ID."),
|
|
4700
5110
|
name: z.string().describe("Collection name."),
|
|
4701
5111
|
row: z.record(z.unknown()).describe("Column-value map for the new row. Server-managed columns are stripped."),
|
|
5112
|
+
idempotency_key: z.string().min(1).max(255).optional().describe("Request-level idempotency key, sent as the Idempotency-Key header. This is separate from any row column named idempotency_key."),
|
|
4702
5113
|
as_system: z.boolean().optional().describe("Write a developer-owned GLOBAL row (user_id = NULL) on a shared:true collection — for content the whole app reads (question banks, lookup tables) rather than data owned by one end-user. Mutually exclusive with a row.user_id. Rejected on non-shared collections."),
|
|
4703
5114
|
on_conflict: z.enum([
|
|
4704
5115
|
"error",
|
|
@@ -4707,7 +5118,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4707
5118
|
]).optional().describe("Conflict policy. \"error\" (default) fails on a unique-constraint clash; \"ignore\" returns the existing row (HTTP 200) unchanged; \"update\" merges the provided columns. Requires conflict_target."),
|
|
4708
5119
|
conflict_target: z.array(z.string()).optional().describe("Columns that form the unique index to conflict on (e.g. [\"user_id\",\"kind\"]). Required when on_conflict is \"ignore\" or \"update\"."),
|
|
4709
5120
|
return_minimal: z.boolean().optional().describe("When true, the response returns only the inserted row id instead of the full row. Use this for rows with large columns (big JSON blobs) to avoid bloating the response — essential for bulk migrations where echoing full rows exhausts the token budget.")
|
|
4710
|
-
}, async ({ project_id, name, row, as_system, on_conflict, conflict_target, return_minimal }, { pat }) => {
|
|
5121
|
+
}, async ({ project_id, name, row, idempotency_key, as_system, on_conflict, conflict_target, return_minimal }, { pat }) => {
|
|
4711
5122
|
const query = {};
|
|
4712
5123
|
if (on_conflict !== void 0) query.on_conflict = on_conflict;
|
|
4713
5124
|
if (conflict_target !== void 0) query.conflict_target = conflict_target.join(",");
|
|
@@ -4716,12 +5127,13 @@ function registerTools$13(server, apiClient) {
|
|
|
4716
5127
|
...row,
|
|
4717
5128
|
as_system: true
|
|
4718
5129
|
} : row;
|
|
4719
|
-
const result = await adminFetch(apiClient, {
|
|
5130
|
+
const result = await adminFetch$1(apiClient, {
|
|
4720
5131
|
method: "POST",
|
|
4721
5132
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
4722
5133
|
body: requestBody,
|
|
4723
5134
|
query: Object.keys(query).length > 0 ? query : void 0,
|
|
4724
|
-
bearer: pat
|
|
5135
|
+
bearer: pat,
|
|
5136
|
+
...idempotency_key !== void 0 && { idempotencyKey: idempotency_key }
|
|
4725
5137
|
});
|
|
4726
5138
|
const warning = catalogShapeWarning(row);
|
|
4727
5139
|
const base = passthroughResult(result);
|
|
@@ -4747,7 +5159,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4747
5159
|
}, async ({ project_id, name, rows, on_conflict, return_minimal }, { pat }) => {
|
|
4748
5160
|
const body = { rows };
|
|
4749
5161
|
if (on_conflict !== void 0) body.on_conflict = on_conflict;
|
|
4750
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5162
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4751
5163
|
method: "POST",
|
|
4752
5164
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows:batch`,
|
|
4753
5165
|
body,
|
|
@@ -4779,7 +5191,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4779
5191
|
if (cursor !== void 0) findQuery.cursor = cursor;
|
|
4780
5192
|
if (select !== void 0) findQuery.select = select;
|
|
4781
5193
|
if (include_deleted !== void 0) findQuery.includeDeleted = include_deleted;
|
|
4782
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5194
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4783
5195
|
method: "GET",
|
|
4784
5196
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
4785
5197
|
query: Object.keys(findQuery).length > 0 ? { query: JSON.stringify(findQuery) } : void 0,
|
|
@@ -4814,7 +5226,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4814
5226
|
if (group_by !== void 0) body.group_by = group_by;
|
|
4815
5227
|
if (where !== void 0) body.where = where;
|
|
4816
5228
|
if (include_deleted !== void 0) body.includeDeleted = include_deleted;
|
|
4817
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5229
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4818
5230
|
method: "POST",
|
|
4819
5231
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows/aggregate`,
|
|
4820
5232
|
body,
|
|
@@ -4824,6 +5236,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4824
5236
|
registerTool(server, apiClient, "amba_admin_update_row", [
|
|
4825
5237
|
"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
5238
|
"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.",
|
|
5239
|
+
"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
5240
|
"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
5241
|
"For updating many rows by a filter instead of one id, use amba_admin_bulk_update.",
|
|
4829
5242
|
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
@@ -4833,15 +5246,17 @@ function registerTools$13(server, apiClient) {
|
|
|
4833
5246
|
name: z.string().describe("Collection name."),
|
|
4834
5247
|
id: z.string().describe("Row UUID to update."),
|
|
4835
5248
|
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
|
-
|
|
5249
|
+
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)."),
|
|
5250
|
+
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).")
|
|
5251
|
+
}, async ({ project_id, name, id, set, expected, objects }, { pat }) => {
|
|
5252
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4839
5253
|
method: "PATCH",
|
|
4840
5254
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows/${encodeURIComponent(id)}`,
|
|
4841
5255
|
body: expected !== void 0 ? {
|
|
4842
5256
|
set,
|
|
4843
5257
|
expected
|
|
4844
5258
|
} : { set },
|
|
5259
|
+
query: objects === "replace" ? { objects: "replace" } : void 0,
|
|
4845
5260
|
bearer: pat
|
|
4846
5261
|
}));
|
|
4847
5262
|
});
|
|
@@ -4858,7 +5273,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4858
5273
|
id: z.string().describe("Row UUID to delete."),
|
|
4859
5274
|
hard: z.boolean().optional().describe("false (default) soft-deletes (sets deleted_at so the row stays recoverable); true permanently removes the row (irreversible).")
|
|
4860
5275
|
}, async ({ project_id, name, id, hard }, { pat }) => {
|
|
4861
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5276
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4862
5277
|
method: "DELETE",
|
|
4863
5278
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows/${encodeURIComponent(id)}`,
|
|
4864
5279
|
query: hard === true ? { hard: "true" } : void 0,
|
|
@@ -4869,21 +5284,24 @@ function registerTools$13(server, apiClient) {
|
|
|
4869
5284
|
"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
5285
|
"`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
5286
|
"`set` is the column→value map applied to every matched row. Server-managed columns are rejected with 400 PROTECTED_COLUMN.",
|
|
5287
|
+
"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
5288
|
"Authenticates as the developer/agent — pass `pat` or send as inbound Bearer.",
|
|
4873
5289
|
"Returns `{data: {updated: <n>, ids: [...]}}`. Errors: 400 WHERE_REQUIRED / PROTECTED_COLUMN / INVALID_COLUMN, 404 COLLECTION_NOT_FOUND."
|
|
4874
5290
|
].join(" "), {
|
|
4875
5291
|
project_id: z.string().describe("The project ID."),
|
|
4876
5292
|
name: z.string().describe("Collection name."),
|
|
4877
5293
|
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
|
-
|
|
5294
|
+
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."),
|
|
5295
|
+
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).")
|
|
5296
|
+
}, async ({ project_id, name, set, where, objects }, { pat }) => {
|
|
5297
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4881
5298
|
method: "PATCH",
|
|
4882
5299
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
4883
5300
|
body: {
|
|
4884
5301
|
set,
|
|
4885
5302
|
where
|
|
4886
5303
|
},
|
|
5304
|
+
query: objects === "replace" ? { objects: "replace" } : void 0,
|
|
4887
5305
|
bearer: pat
|
|
4888
5306
|
}));
|
|
4889
5307
|
});
|
|
@@ -4903,7 +5321,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4903
5321
|
}, async ({ project_id, name, where, confirm, hard }, { pat }) => {
|
|
4904
5322
|
const query = { confirm: String(confirm) };
|
|
4905
5323
|
if (hard === true) query.hard = "true";
|
|
4906
|
-
return passthroughResult(await adminFetch(apiClient, {
|
|
5324
|
+
return passthroughResult(await adminFetch$1(apiClient, {
|
|
4907
5325
|
method: "DELETE",
|
|
4908
5326
|
path: `/projects/${project_id}/collections/${encodeURIComponent(name)}/rows`,
|
|
4909
5327
|
body: { where },
|
|
@@ -4919,6 +5337,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4919
5337
|
].join(" "), {
|
|
4920
5338
|
name: z.string().describe("Collection name."),
|
|
4921
5339
|
row: z.record(z.unknown()).describe("Column-value map. Server-managed columns and user_id are stripped/overridden."),
|
|
5340
|
+
idempotency_key: z.string().min(1).max(255).optional().describe("Request-level idempotency key, sent as the Idempotency-Key header. This is separate from any row column named idempotency_key."),
|
|
4922
5341
|
on_conflict: z.enum([
|
|
4923
5342
|
"error",
|
|
4924
5343
|
"ignore",
|
|
@@ -4927,18 +5346,19 @@ function registerTools$13(server, apiClient) {
|
|
|
4927
5346
|
conflict_target: z.array(z.string()).optional().describe("Unique-index columns to conflict on (e.g. [\"user_id\",\"kind\"]). Required for ignore/update."),
|
|
4928
5347
|
api_key: z.string().describe("Project client X-Api-Key. End-user tools authenticate via this + an optional session token, NOT via a developer PAT."),
|
|
4929
5348
|
session_token: z.string().optional().describe("Optional app_user session Bearer token. Required by the route's clientSessionAuth.")
|
|
4930
|
-
}, async ({ name, row, on_conflict, conflict_target, api_key, session_token }) => {
|
|
5349
|
+
}, async ({ name, row, idempotency_key, on_conflict, conflict_target, api_key, session_token }) => {
|
|
4931
5350
|
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
4932
5351
|
const query = {};
|
|
4933
5352
|
if (on_conflict !== void 0) query.on_conflict = on_conflict;
|
|
4934
5353
|
if (conflict_target !== void 0) query.conflict_target = conflict_target.join(",");
|
|
4935
|
-
return passthroughResult(await clientFetch(apiClient, {
|
|
5354
|
+
return passthroughResult(await clientFetch$1(apiClient, {
|
|
4936
5355
|
method: "POST",
|
|
4937
5356
|
path: `/collections/${encodeURIComponent(name)}`,
|
|
4938
5357
|
body: row,
|
|
4939
5358
|
...Object.keys(query).length > 0 && { query },
|
|
4940
5359
|
apiKey: api_key,
|
|
4941
|
-
...session_token !== void 0 && { sessionToken: session_token }
|
|
5360
|
+
...session_token !== void 0 && { sessionToken: session_token },
|
|
5361
|
+
...idempotency_key !== void 0 && { idempotencyKey: idempotency_key }
|
|
4942
5362
|
}));
|
|
4943
5363
|
});
|
|
4944
5364
|
server.tool("amba_client_list_rows", [
|
|
@@ -4966,7 +5386,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4966
5386
|
if (cursor !== void 0) findQuery.cursor = cursor;
|
|
4967
5387
|
if (select !== void 0) findQuery.select = select;
|
|
4968
5388
|
if (include_deleted !== void 0) findQuery.includeDeleted = include_deleted;
|
|
4969
|
-
return passthroughResult(await clientFetch(apiClient, {
|
|
5389
|
+
return passthroughResult(await clientFetch$1(apiClient, {
|
|
4970
5390
|
method: "GET",
|
|
4971
5391
|
path: `/collections/${encodeURIComponent(name)}`,
|
|
4972
5392
|
query: Object.keys(findQuery).length > 0 ? { query: JSON.stringify(findQuery) } : void 0,
|
|
@@ -4985,7 +5405,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4985
5405
|
session_token: z.string().optional().describe("App_user session Bearer. Required by the route.")
|
|
4986
5406
|
}, async ({ name, id, api_key, session_token }) => {
|
|
4987
5407
|
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
4988
|
-
return passthroughResult(await clientFetch(apiClient, {
|
|
5408
|
+
return passthroughResult(await clientFetch$1(apiClient, {
|
|
4989
5409
|
method: "GET",
|
|
4990
5410
|
path: `/collections/${encodeURIComponent(name)}/${encodeURIComponent(id)}`,
|
|
4991
5411
|
apiKey: api_key,
|
|
@@ -4994,6 +5414,7 @@ function registerTools$13(server, apiClient) {
|
|
|
4994
5414
|
});
|
|
4995
5415
|
server.tool("amba_client_update_row", [
|
|
4996
5416
|
"Update a single row by id. Auto-RLS — only succeeds if the row belongs to the signed-in app_user.",
|
|
5417
|
+
"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
5418
|
"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
5419
|
"For bulk updates with a where-clause, omit `id` and pass `where` + `set`.",
|
|
4999
5420
|
"Server-managed columns and user_id are rejected with 400 INVALID_COLUMN.",
|
|
@@ -5005,17 +5426,20 @@ function registerTools$13(server, apiClient) {
|
|
|
5005
5426
|
set: setSchema.describe("Column-value map to write. Required."),
|
|
5006
5427
|
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
5428
|
limit: z.number().optional().describe("Bulk-update cap (default 1000, max 10000). Ignored when `id` is set."),
|
|
5429
|
+
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
5430
|
api_key: z.string().describe("Project client X-Api-Key."),
|
|
5009
5431
|
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 }) => {
|
|
5432
|
+
}, async ({ name, id, where, set, expected, limit, objects, api_key, session_token }) => {
|
|
5011
5433
|
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
5012
|
-
|
|
5434
|
+
const objectsQuery = objects === "replace" ? { objects: "replace" } : void 0;
|
|
5435
|
+
if (id !== void 0) return passthroughResult(await clientFetch$1(apiClient, {
|
|
5013
5436
|
method: "PATCH",
|
|
5014
5437
|
path: `/collections/${encodeURIComponent(name)}/${encodeURIComponent(id)}`,
|
|
5015
5438
|
body: expected !== void 0 ? {
|
|
5016
5439
|
set,
|
|
5017
5440
|
expected
|
|
5018
5441
|
} : { set },
|
|
5442
|
+
...objectsQuery !== void 0 && { query: objectsQuery },
|
|
5019
5443
|
apiKey: api_key,
|
|
5020
5444
|
...session_token !== void 0 && { sessionToken: session_token }
|
|
5021
5445
|
}));
|
|
@@ -5024,10 +5448,11 @@ function registerTools$13(server, apiClient) {
|
|
|
5024
5448
|
where: where ?? {}
|
|
5025
5449
|
};
|
|
5026
5450
|
if (limit !== void 0) body.limit = limit;
|
|
5027
|
-
return passthroughResult(await clientFetch(apiClient, {
|
|
5451
|
+
return passthroughResult(await clientFetch$1(apiClient, {
|
|
5028
5452
|
method: "PATCH",
|
|
5029
5453
|
path: `/collections/${encodeURIComponent(name)}`,
|
|
5030
5454
|
body,
|
|
5455
|
+
...objectsQuery !== void 0 && { query: objectsQuery },
|
|
5031
5456
|
apiKey: api_key,
|
|
5032
5457
|
...session_token !== void 0 && { sessionToken: session_token }
|
|
5033
5458
|
}));
|
|
@@ -5045,7 +5470,7 @@ function registerTools$13(server, apiClient) {
|
|
|
5045
5470
|
session_token: z.string().optional().describe("App_user session Bearer. Required by the route.")
|
|
5046
5471
|
}, async ({ name, id, where, limit, api_key, session_token }) => {
|
|
5047
5472
|
if (!api_key) return MISSING_API_KEY_ERROR;
|
|
5048
|
-
if (id !== void 0) return passthroughResult(await clientFetch(apiClient, {
|
|
5473
|
+
if (id !== void 0) return passthroughResult(await clientFetch$1(apiClient, {
|
|
5049
5474
|
method: "DELETE",
|
|
5050
5475
|
path: `/collections/${encodeURIComponent(name)}/${encodeURIComponent(id)}`,
|
|
5051
5476
|
apiKey: api_key,
|
|
@@ -5053,7 +5478,7 @@ function registerTools$13(server, apiClient) {
|
|
|
5053
5478
|
}));
|
|
5054
5479
|
const body = { where: where ?? {} };
|
|
5055
5480
|
if (limit !== void 0) body.limit = limit;
|
|
5056
|
-
return passthroughResult(await clientFetch(apiClient, {
|
|
5481
|
+
return passthroughResult(await clientFetch$1(apiClient, {
|
|
5057
5482
|
method: "DELETE",
|
|
5058
5483
|
path: `/collections/${encodeURIComponent(name)}`,
|
|
5059
5484
|
body,
|
|
@@ -5076,7 +5501,7 @@ function registerTools$13(server, apiClient) {
|
|
|
5076
5501
|
const findQuery = {};
|
|
5077
5502
|
if (where !== void 0) findQuery.where = where;
|
|
5078
5503
|
if (include_deleted !== void 0) findQuery.includeDeleted = include_deleted;
|
|
5079
|
-
return passthroughResult(await clientFetch(apiClient, {
|
|
5504
|
+
return passthroughResult(await clientFetch$1(apiClient, {
|
|
5080
5505
|
method: "GET",
|
|
5081
5506
|
path: `/collections/${encodeURIComponent(name)}/count`,
|
|
5082
5507
|
query: Object.keys(findQuery).length > 0 ? { query: JSON.stringify(findQuery) } : void 0,
|
|
@@ -5116,7 +5541,7 @@ function registerTools$13(server, apiClient) {
|
|
|
5116
5541
|
if (cursor !== void 0) body.cursor = cursor;
|
|
5117
5542
|
if (select !== void 0) body.select = select;
|
|
5118
5543
|
if (include_deleted !== void 0) body.include_deleted = include_deleted;
|
|
5119
|
-
return passthroughResult(await clientFetch(apiClient, {
|
|
5544
|
+
return passthroughResult(await clientFetch$1(apiClient, {
|
|
5120
5545
|
method: "POST",
|
|
5121
5546
|
path: `/collections/${encodeURIComponent(name)}/find`,
|
|
5122
5547
|
body,
|
|
@@ -5160,7 +5585,7 @@ function registerTools$13(server, apiClient) {
|
|
|
5160
5585
|
if (where !== void 0) body.where = where;
|
|
5161
5586
|
if (cursor !== void 0) body.cursor = cursor;
|
|
5162
5587
|
if (include_deleted !== void 0) body.include_deleted = include_deleted;
|
|
5163
|
-
return passthroughResult(await clientFetch(apiClient, {
|
|
5588
|
+
return passthroughResult(await clientFetch$1(apiClient, {
|
|
5164
5589
|
method: "POST",
|
|
5165
5590
|
path: `/collections/${encodeURIComponent(name)}/find-nearest`,
|
|
5166
5591
|
body,
|
|
@@ -5201,7 +5626,7 @@ function registerTools$13(server, apiClient) {
|
|
|
5201
5626
|
if (filter !== void 0) body.filter = filter;
|
|
5202
5627
|
if (where !== void 0) body.where = where;
|
|
5203
5628
|
if (include_deleted !== void 0) body.include_deleted = include_deleted;
|
|
5204
|
-
return passthroughResult(await clientFetch(apiClient, {
|
|
5629
|
+
return passthroughResult(await clientFetch$1(apiClient, {
|
|
5205
5630
|
method: "POST",
|
|
5206
5631
|
path: `/collections/${encodeURIComponent(name)}/aggregate`,
|
|
5207
5632
|
body,
|
|
@@ -5324,7 +5749,7 @@ function jsonResult(payload) {
|
|
|
5324
5749
|
* of a generic upstream 400.
|
|
5325
5750
|
*/
|
|
5326
5751
|
const MAX_BUNDLE_BYTES = 10 * 1024 * 1024;
|
|
5327
|
-
function registerTools$
|
|
5752
|
+
function registerTools$18(server, apiClient) {
|
|
5328
5753
|
registerTool(server, apiClient, "amba_functions_deploy", [
|
|
5329
5754
|
"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
5755
|
"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).",
|
|
@@ -5397,6 +5822,42 @@ function registerTools$12(server, apiClient) {
|
|
|
5397
5822
|
throw err;
|
|
5398
5823
|
}
|
|
5399
5824
|
}, ["amba_delete_function"]);
|
|
5825
|
+
registerTool(server, apiClient, "amba_function_domains_attach", [
|
|
5826
|
+
"Attach one exact custom hostname to one deployed function. The complete incoming path and query string are preserved when the hostname routes to the function.",
|
|
5827
|
+
"MVP constraints: exact lowercase hostnames only; no wildcards or path rewrites. Attaching a subdomain does NOT create a `www` hostname automatically.",
|
|
5828
|
+
"The response contains provider-neutral certificate/ownership status plus DNS validation instructions. Publish the returned CNAME and validation records, then call `amba_function_domains_refresh` until `live=true` (both statuses active and routing reconciliation successful).",
|
|
5829
|
+
"MVP limits use the effective project tier (free 1, pro 5, scale 20, enterprise or comped 50), with five attach attempts per project per hour. An unverified claim becomes eligible for exact-host reclaim after 24 hours; Worker route/KV allocation waits for active ownership.",
|
|
5830
|
+
"Podcast/feed clients cannot supply an Amba API key. Their function must be deployed with `public: true`; validate any application-specific private token in the handler."
|
|
5831
|
+
].join(" "), {
|
|
5832
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5833
|
+
name: z.string().describe("Deployed function name."),
|
|
5834
|
+
hostname: z.string().describe("One exact lowercase hostname, for example feeds.podpod.host.")
|
|
5835
|
+
}, async ({ project_id, name, hostname }, { pat }) => {
|
|
5836
|
+
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/functions/${encodeURIComponent(name)}/domains`, { body: { hostname } }));
|
|
5837
|
+
});
|
|
5838
|
+
registerTool(server, apiClient, "amba_function_domains_list", "List exact custom hostnames attached to one function. Status is provider-neutral: `live=true` only when `cert_status` and `ownership_status` are active and required routing reconciliation succeeded.", {
|
|
5839
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5840
|
+
name: z.string().describe("Deployed function name.")
|
|
5841
|
+
}, async ({ project_id, name }, { pat }) => {
|
|
5842
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/functions/${encodeURIComponent(name)}/domains`));
|
|
5843
|
+
});
|
|
5844
|
+
registerTool(server, apiClient, "amba_function_domains_refresh", "Refresh DNS ownership and certificate state for one exact function hostname. Publish any returned validation records and repeat until `live=true`.", {
|
|
5845
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5846
|
+
name: z.string().describe("Deployed function name."),
|
|
5847
|
+
hostname: z.string().describe("Exact attached hostname to refresh.")
|
|
5848
|
+
}, async ({ project_id, name, hostname }, { pat }) => {
|
|
5849
|
+
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/functions/${encodeURIComponent(name)}/domains/${encodeURIComponent(hostname)}/refresh`));
|
|
5850
|
+
});
|
|
5851
|
+
registerTool(server, apiClient, "amba_function_domains_remove", "Detach one exact custom hostname from a function. Requests to the detached hostname fail closed after routing propagation.", {
|
|
5852
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5853
|
+
name: z.string().describe("Deployed function name."),
|
|
5854
|
+
hostname: z.string().describe("Exact attached hostname to detach.")
|
|
5855
|
+
}, async ({ project_id, name, hostname }, { pat }) => {
|
|
5856
|
+
return jsonResult(await callWithPat(apiClient, pat, "DELETE", `/projects/${encodeURIComponent(project_id)}/functions/${encodeURIComponent(name)}/domains/${encodeURIComponent(hostname)}`) ?? {
|
|
5857
|
+
hostname,
|
|
5858
|
+
deleted: true
|
|
5859
|
+
});
|
|
5860
|
+
});
|
|
5400
5861
|
registerTool(server, apiClient, "amba_functions_schedule", [
|
|
5401
5862
|
"Register a cron Schedule for a function. The schedule is keyed deterministically per (project, function) — re-calling with a different cron/timezone replaces the prior schedule rather than creating a duplicate.",
|
|
5402
5863
|
"Cron must be a standard 5- or 6-field whitespace-separated expression. `timezone` defaults to \"UTC\" if omitted. Timezones are IANA strings (e.g. \"America/Los_Angeles\").",
|
|
@@ -5435,24 +5896,29 @@ function registerTools$12(server, apiClient) {
|
|
|
5435
5896
|
registerTool(server, apiClient, "amba_functions_get_logs", [
|
|
5436
5897
|
"Read function logs for one function over a bounded time range.",
|
|
5437
5898
|
"Default range: last 1 hour. Max range: last 24 hours. `since` and `until` are ISO 8601 timestamps.",
|
|
5438
|
-
"Default limit: 100 events. Max: 1000. Larger spans require operator-side queries — this tool is for \"tail my function\" UX."
|
|
5899
|
+
"Default limit: 100 events. Max: 1000. Larger spans require operator-side queries — this tool is for \"tail my function\" UX.",
|
|
5900
|
+
"Debugging one request? `since_request_id` starts the range at the oldest event mentioning that request id (\"everything from this request onward\"); `request_id` instead filters to only events mentioning it. The two compose."
|
|
5439
5901
|
].join(" "), {
|
|
5440
5902
|
project_id: z.string().describe("The Amba project ID."),
|
|
5441
5903
|
name: z.string().describe("Function name to read logs for."),
|
|
5442
5904
|
since: z.string().optional().describe("ISO 8601 lower bound (inclusive). Defaults to `until - 1h`."),
|
|
5443
5905
|
until: z.string().optional().describe("ISO 8601 upper bound (exclusive). Defaults to now."),
|
|
5444
|
-
limit: z.number().int().min(1).max(1e3).optional().describe("Max events (1-1000, default 100).")
|
|
5445
|
-
|
|
5906
|
+
limit: z.number().int().min(1).max(1e3).optional().describe("Max events (1-1000, default 100)."),
|
|
5907
|
+
request_id: z.string().optional().describe("Only return events whose log/exception text mentions this request id (r_...)."),
|
|
5908
|
+
since_request_id: z.string().optional().describe("Anchor the range start at the oldest event mentioning this request id (r_...). Searched within the last 24h, or within since/until when given. 404s if no event mentions it.")
|
|
5909
|
+
}, async ({ project_id, name, since, until, limit, request_id, since_request_id }, { pat }) => {
|
|
5446
5910
|
const query = {};
|
|
5447
5911
|
if (since !== void 0) query.since = since;
|
|
5448
5912
|
if (until !== void 0) query.until = until;
|
|
5449
5913
|
if (limit !== void 0) query.limit = String(limit);
|
|
5914
|
+
if (request_id !== void 0) query.request_id = request_id;
|
|
5915
|
+
if (since_request_id !== void 0) query.since_request_id = since_request_id;
|
|
5450
5916
|
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/functions/${encodeURIComponent(name)}/logs`, { query }));
|
|
5451
5917
|
}, ["amba_get_function_logs"]);
|
|
5452
5918
|
}
|
|
5453
5919
|
//#endregion
|
|
5454
5920
|
//#region src/tools/sites.ts
|
|
5455
|
-
function registerTools$
|
|
5921
|
+
function registerTools$17(server, apiClient) {
|
|
5456
5922
|
registerTool(server, apiClient, "amba_sites_deploy", [
|
|
5457
5923
|
"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
5924
|
"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`.",
|
|
@@ -5521,6 +5987,14 @@ function registerTools$11(server, apiClient) {
|
|
|
5521
5987
|
}, async ({ project_id, name }, { pat }) => {
|
|
5522
5988
|
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/sites/${encodeURIComponent(name)}/domains`));
|
|
5523
5989
|
}, ["amba_list_site_domains"]);
|
|
5990
|
+
registerTool(server, apiClient, "amba_sites_refresh_domain", "Force a fresh ownership and certificate lifecycle check for a site hostname. Returns both normalized states plus current validation records.", {
|
|
5991
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
5992
|
+
name: z.string().describe("Site name."),
|
|
5993
|
+
hostname: z.string().describe("Hostname to refresh. Lowercased at the tool boundary.")
|
|
5994
|
+
}, async ({ project_id, name, hostname }, { pat }) => {
|
|
5995
|
+
const normalisedHostname = hostname.toLowerCase();
|
|
5996
|
+
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/sites/${encodeURIComponent(name)}/domains/${encodeURIComponent(normalisedHostname)}/refresh-cert`));
|
|
5997
|
+
}, ["amba_refresh_site_domain"]);
|
|
5524
5998
|
registerTool(server, apiClient, "amba_sites_remove_domain", "Detach a custom hostname from a site. The upstream binding is dropped server-side; the DB row is removed; the host:<hostname> KV entry is cleared. Idempotent — removing a hostname that is no longer attached returns success.", {
|
|
5525
5999
|
project_id: z.string().describe("The Amba project ID."),
|
|
5526
6000
|
name: z.string().describe("Site name."),
|
|
@@ -5544,7 +6018,7 @@ function registerTools$11(server, apiClient) {
|
|
|
5544
6018
|
}
|
|
5545
6019
|
//#endregion
|
|
5546
6020
|
//#region src/tools/domains.ts
|
|
5547
|
-
function registerTools$
|
|
6021
|
+
function registerTools$16(server, apiClient) {
|
|
5548
6022
|
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
6023
|
project_id: z.string().describe("The Amba project ID."),
|
|
5550
6024
|
query: z.string().describe("A keyword (e.g. \"getunbury\") or a full domain (e.g. \"unbury.com\")."),
|
|
@@ -5597,10 +6071,63 @@ function registerTools$10(server, apiClient) {
|
|
|
5597
6071
|
if (offset !== void 0) query.offset = String(offset);
|
|
5598
6072
|
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/domains`, { query }));
|
|
5599
6073
|
});
|
|
6074
|
+
registerTool(server, apiClient, "amba_domains_email_status", "Get inbound email-routing status for a domain purchased through Amba.", {
|
|
6075
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6076
|
+
domain: z.string().describe("Purchased domain, e.g. \"example.com\".")
|
|
6077
|
+
}, async ({ project_id, domain }, { pat }) => jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/domains/${encodeURIComponent(domain.toLowerCase())}/email`)));
|
|
6078
|
+
registerTool(server, apiClient, "amba_domains_email_enable", ["Enable free inbound email routing for a domain purchased through Amba.", "This safely refuses when the domain already has MX records for another mail provider, so it cannot silently replace an existing mailbox service."].join(" "), {
|
|
6079
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6080
|
+
domain: z.string().describe("Purchased domain, e.g. \"example.com\".")
|
|
6081
|
+
}, async ({ project_id, domain }, { pat }) => jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/domains/${encodeURIComponent(domain.toLowerCase())}/email/enable`, { body: {} })));
|
|
6082
|
+
registerTool(server, apiClient, "amba_domains_email_destinations_add", ["Add a mailbox that can receive forwarded mail for a purchased domain.", "The mailbox owner must click the verification email before a forward can activate; pending results return action_required:true."].join(" "), {
|
|
6083
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6084
|
+
domain: z.string().describe("Purchased domain."),
|
|
6085
|
+
email: z.string().email().describe("External mailbox that will receive forwarded mail.")
|
|
6086
|
+
}, async ({ project_id, domain, email }, { pat }) => jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/domains/${encodeURIComponent(domain.toLowerCase())}/email/destinations`, { body: { email: email.toLowerCase() } })));
|
|
6087
|
+
registerTool(server, apiClient, "amba_domains_email_destinations_list", "List only this project’s forwarding destinations associated with a purchased domain.", {
|
|
6088
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6089
|
+
domain: z.string().describe("Purchased domain.")
|
|
6090
|
+
}, async ({ project_id, domain }, { pat }) => jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/domains/${encodeURIComponent(domain.toLowerCase())}/email/destinations`)));
|
|
6091
|
+
registerTool(server, apiClient, "amba_domains_email_destinations_get", "Poll a forwarding destination after its owner clicks the verification email.", {
|
|
6092
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6093
|
+
domain: z.string().describe("Purchased domain."),
|
|
6094
|
+
destination_id: z.string().describe("Destination ID returned by the add/list tool.")
|
|
6095
|
+
}, async ({ project_id, domain, destination_id }, { pat }) => jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/domains/${encodeURIComponent(domain.toLowerCase())}/email/destinations/${encodeURIComponent(destination_id)}`)));
|
|
6096
|
+
registerTool(server, apiClient, "amba_domains_email_forwards_set", ["Create or update a literal email forward such as support@example.com → owner@gmail.com.", "The tool enables routing if safe and starts destination verification when needed. If action_required:true is returned, have the mailbox owner click the verification email, poll the destination, then repeat this same call."].join(" "), {
|
|
6097
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6098
|
+
domain: z.string().describe("Purchased domain."),
|
|
6099
|
+
source: z.string().describe("Local part or full address on the domain, e.g. \"support\" or \"support@example.com\"."),
|
|
6100
|
+
destination: z.string().email().describe("Verified external mailbox to forward into.")
|
|
6101
|
+
}, async ({ project_id, domain, source, destination }, { pat }) => jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/domains/${encodeURIComponent(domain.toLowerCase())}/email/forwards`, { body: {
|
|
6102
|
+
source,
|
|
6103
|
+
destination: destination.toLowerCase()
|
|
6104
|
+
} })));
|
|
6105
|
+
registerTool(server, apiClient, "amba_domains_email_forwards_list", "List literal email forwards configured on a purchased domain.", {
|
|
6106
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6107
|
+
domain: z.string().describe("Purchased domain.")
|
|
6108
|
+
}, async ({ project_id, domain }, { pat }) => jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/domains/${encodeURIComponent(domain.toLowerCase())}/email/forwards`)));
|
|
6109
|
+
registerTool(server, apiClient, "amba_domains_email_forwards_delete", "Delete a literal email forward from a purchased domain.", {
|
|
6110
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6111
|
+
domain: z.string().describe("Purchased domain."),
|
|
6112
|
+
forward_id: z.string().describe("Forward ID returned by the set/list tool.")
|
|
6113
|
+
}, async ({ project_id, domain, forward_id }, { pat }) => jsonResult(await callWithPat(apiClient, pat, "DELETE", `/projects/${encodeURIComponent(project_id)}/domains/${encodeURIComponent(domain.toLowerCase())}/email/forwards/${encodeURIComponent(forward_id)}`)));
|
|
6114
|
+
registerTool(server, apiClient, "amba_domains_email_catch_all_get", "Get the catch-all forwarding rule for a purchased domain.", {
|
|
6115
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6116
|
+
domain: z.string().describe("Purchased domain.")
|
|
6117
|
+
}, async ({ project_id, domain }, { pat }) => jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/domains/${encodeURIComponent(domain.toLowerCase())}/email/catch-all`)));
|
|
6118
|
+
registerTool(server, apiClient, "amba_domains_email_catch_all_set", ["Enable, change, or disable catch-all forwarding for every unmatched address on a purchased domain.", "When enabling, destination is required and must be verified. A pending mailbox returns action_required:true."].join(" "), {
|
|
6119
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6120
|
+
domain: z.string().describe("Purchased domain."),
|
|
6121
|
+
enabled: z.boolean().describe("True to enable/update catch-all; false to disable it."),
|
|
6122
|
+
destination: z.string().email().optional().describe("External mailbox. Required when enabled=true; omit when disabling.")
|
|
6123
|
+
}, async ({ project_id, domain, enabled, destination }, { pat }) => jsonResult(await callWithPat(apiClient, pat, "PUT", `/projects/${encodeURIComponent(project_id)}/domains/${encodeURIComponent(domain.toLowerCase())}/email/catch-all`, { body: {
|
|
6124
|
+
enabled,
|
|
6125
|
+
...destination ? { destination: destination.toLowerCase() } : {}
|
|
6126
|
+
} })));
|
|
5600
6127
|
}
|
|
5601
6128
|
//#endregion
|
|
5602
6129
|
//#region src/tools/operations.ts
|
|
5603
|
-
function registerTools$
|
|
6130
|
+
function registerTools$15(server, apiClient) {
|
|
5604
6131
|
registerTool(server, apiClient, "amba_operations_get", [
|
|
5605
6132
|
"Poll an async operation by its operation_id to see whether it has finished.",
|
|
5606
6133
|
"Returns the operation `status` (pending | running | succeeded | failed); on `failed`, `failed_reason` says why, and on `succeeded`, `result` holds the outcome.",
|
|
@@ -5633,17 +6160,24 @@ function registerTools$9(server, apiClient) {
|
|
|
5633
6160
|
}
|
|
5634
6161
|
//#endregion
|
|
5635
6162
|
//#region src/tools/secrets.ts
|
|
5636
|
-
|
|
6163
|
+
const SET_SECRET_NAME = z.string().regex(/^[A-Z][A-Z0-9_]{0,62}$/, "Secret name must match /^[A-Z][A-Z0-9_]{0,62}$/ (uppercase, starts with a letter, max 63 characters)").superRefine((name, ctx) => {
|
|
6164
|
+
const reason = getBindingReservationReason(name);
|
|
6165
|
+
if (reason !== null) ctx.addIssue({
|
|
6166
|
+
code: z.ZodIssueCode.custom,
|
|
6167
|
+
message: reason
|
|
6168
|
+
});
|
|
6169
|
+
});
|
|
6170
|
+
function registerTools$14(server, apiClient) {
|
|
5637
6171
|
registerTool(server, apiClient, "amba_secrets_set", [
|
|
5638
6172
|
"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
6173
|
"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).",
|
|
5640
6174
|
"Secret name must match /^[A-Z][A-Z0-9_]{0,62}$/. Function name (when given) must match /^[a-z][a-z0-9_-]{0,57}$/.",
|
|
5641
|
-
"
|
|
6175
|
+
"The complete AMBA_ and EDGE_ namespaces, plus the exact names STORAGE and EDGE_DB_PROXY, are platform-managed. MCP rejects them before an API/provider call; the API also returns 400 RESERVED_BINDING.",
|
|
5642
6176
|
"Secrets may be set BEFORE deploying the function — the sync drains once a deployment lands, so you can set every secret first and deploy once.",
|
|
5643
6177
|
"Value cap: 64 KiB. Plaintext is never returned afterwards."
|
|
5644
6178
|
].join(" "), {
|
|
5645
6179
|
project_id: z.string().describe("The Amba project ID."),
|
|
5646
|
-
name:
|
|
6180
|
+
name: SET_SECRET_NAME.describe("Secret name. UPPER_SNAKE_CASE. The AMBA_ and EDGE_ prefixes and exact names STORAGE and EDGE_DB_PROXY are reserved."),
|
|
5647
6181
|
value: z.string().describe("Secret value. Stored encrypted at rest; never returned in plaintext afterwards."),
|
|
5648
6182
|
function: z.string().optional().describe("Function this secret binds to. OMIT for a project-wide secret (applied to every function in the project).")
|
|
5649
6183
|
}, async ({ project_id, name, value, function: functionName }, { pat }) => {
|
|
@@ -5675,7 +6209,7 @@ function registerTools$8(server, apiClient) {
|
|
|
5675
6209
|
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/secrets`));
|
|
5676
6210
|
}, ["amba_list_secrets"]);
|
|
5677
6211
|
registerTool(server, apiClient, "amba_secrets_delete", [
|
|
5678
|
-
"Delete a secret. Removes the
|
|
6212
|
+
"Delete a secret. Removes the backing-store value and durably queues runtime-binding revocation across every affected Worker script. Provider failures retry automatically; the control row remains visible with a revocation status until cleanup succeeds.",
|
|
5679
6213
|
"Pass `function` to delete a function-scoped secret; OMIT it to delete the project-wide secret of this name.",
|
|
5680
6214
|
"Idempotent: deleting a secret that no longer exists succeeds."
|
|
5681
6215
|
].join(" "), {
|
|
@@ -5687,6 +6221,7 @@ function registerTools$8(server, apiClient) {
|
|
|
5687
6221
|
try {
|
|
5688
6222
|
return jsonResult(await callWithPat(apiClient, pat, "DELETE", `/projects/${encodeURIComponent(project_id)}/secrets/${encodeURIComponent(name)}`, functionName === void 0 ? {} : { query: { function: functionName } }) ?? {
|
|
5689
6223
|
deleted: true,
|
|
6224
|
+
revocation_queued: true,
|
|
5690
6225
|
name,
|
|
5691
6226
|
function: scope
|
|
5692
6227
|
});
|
|
@@ -5706,27 +6241,259 @@ function registerTools$8(server, apiClient) {
|
|
|
5706
6241
|
const PROVIDER_ENUM$1 = z.enum([
|
|
5707
6242
|
"anthropic",
|
|
5708
6243
|
"openai",
|
|
5709
|
-
"mistral"
|
|
6244
|
+
"mistral",
|
|
6245
|
+
"gemini"
|
|
5710
6246
|
]);
|
|
5711
6247
|
const RATE_LIMIT_SHAPE = z.object({
|
|
5712
6248
|
window: z.string().optional().describe("Window length (e.g. \"60s\", \"1m\", \"1h\"). Defaults to \"60s\"."),
|
|
5713
6249
|
max: z.number().int().min(1).optional().describe("Max invocations per window. Defaults to 20."),
|
|
5714
6250
|
key: z.string().optional().describe("Bucketing key. Typically \"user_id\" or \"session_id\". Defaults to \"user_id\".")
|
|
5715
6251
|
}).strict();
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
6252
|
+
const DEFAULT_IMAGE_MIME = "image/png";
|
|
6253
|
+
/**
|
|
6254
|
+
* Normalize an image's `url` / `data` to non-empty values, trimming whitespace
|
|
6255
|
+
* and treating a blank result as absent. Returns `null` when NEITHER a real url
|
|
6256
|
+
* nor real data survives — such a block carries no image and is dropped by the
|
|
6257
|
+
* caller (instead of emitting an empty image URL or an empty base64 block).
|
|
6258
|
+
*/
|
|
6259
|
+
function normalizeImage(img) {
|
|
6260
|
+
const url = typeof img.url === "string" && img.url.trim().length > 0 ? img.url.trim() : void 0;
|
|
6261
|
+
const data = typeof img.data === "string" && img.data.trim().length > 0 ? img.data.trim() : void 0;
|
|
6262
|
+
if (!url && !data) return null;
|
|
6263
|
+
return {
|
|
6264
|
+
url,
|
|
6265
|
+
data,
|
|
6266
|
+
mime: img.mime
|
|
6267
|
+
};
|
|
6268
|
+
}
|
|
6269
|
+
/**
|
|
6270
|
+
* Map a NORMALIZED vendor-neutral image into the provider's native
|
|
6271
|
+
* content-block shape. Anthropic → `{type:'image', source:{...}}`;
|
|
6272
|
+
* OpenAI/Mistral → `{type:'image_url', image_url:{url}}` (base64 becomes a
|
|
6273
|
+
* data: URL); Gemini → a `parts[]` entry (`inline_data` for base64,
|
|
6274
|
+
* `file_data` for a URL). The input is guaranteed to have a real `url` or
|
|
6275
|
+
* real `data`.
|
|
6276
|
+
*
|
|
6277
|
+
* Note: Gemini's image shape is a `contents[].parts[]` entry, NOT a message
|
|
6278
|
+
* `content[]` block — so the Gemini path builds `contents[]` directly via
|
|
6279
|
+
* `geminiContentsFromMessages` and does NOT route image mapping through
|
|
6280
|
+
* `attachImagesToMessages` (which speaks the OpenAI/Anthropic message-content
|
|
6281
|
+
* shape). This helper is reused there for the per-part mapping.
|
|
6282
|
+
*/
|
|
6283
|
+
function imageBlockForProvider(img, provider) {
|
|
6284
|
+
const mime = img.mime ?? DEFAULT_IMAGE_MIME;
|
|
6285
|
+
if (provider === "anthropic") return img.url ? {
|
|
6286
|
+
type: "image",
|
|
6287
|
+
source: {
|
|
6288
|
+
type: "url",
|
|
6289
|
+
url: img.url
|
|
6290
|
+
}
|
|
6291
|
+
} : {
|
|
6292
|
+
type: "image",
|
|
6293
|
+
source: {
|
|
6294
|
+
type: "base64",
|
|
6295
|
+
media_type: mime,
|
|
6296
|
+
data: img.data ?? ""
|
|
6297
|
+
}
|
|
6298
|
+
};
|
|
6299
|
+
if (provider === "gemini") return img.url ? { file_data: {
|
|
6300
|
+
mime_type: mime,
|
|
6301
|
+
file_uri: img.url
|
|
6302
|
+
} } : { inline_data: {
|
|
6303
|
+
mime_type: mime,
|
|
6304
|
+
data: img.data ?? ""
|
|
6305
|
+
} };
|
|
6306
|
+
return {
|
|
6307
|
+
type: "image_url",
|
|
6308
|
+
image_url: { url: img.url ?? `data:${mime};base64,${img.data ?? ""}` }
|
|
6309
|
+
};
|
|
6310
|
+
}
|
|
6311
|
+
/**
|
|
6312
|
+
* Convert ONE content block from a message's content array into the provider's
|
|
6313
|
+
* native shape. A vendor-neutral `{type:'image', url|data}` block is mapped via
|
|
6314
|
+
* `imageBlockForProvider`; everything else (a `{type:'text'}` block, or an
|
|
6315
|
+
* already-native block the agent supplied) passes through unchanged. This keeps
|
|
6316
|
+
* the whole message in a SINGLE format so we never mix vendor-neutral and
|
|
6317
|
+
* pre-mapped native image blocks in one message (which would be malformed for
|
|
6318
|
+
* the raw `/ai/messages` passthrough).
|
|
6319
|
+
*
|
|
6320
|
+
* Returns `null` only for a vendor-neutral image block that normalizes away
|
|
6321
|
+
* (blank url + blank data) — the caller drops it.
|
|
6322
|
+
*/
|
|
6323
|
+
function nativeContentBlock(block, provider) {
|
|
6324
|
+
if (typeof block === "object" && block !== null) {
|
|
6325
|
+
const b = block;
|
|
6326
|
+
if (b["type"] === "image" && ("url" in b || "data" in b) && !("source" in b)) {
|
|
6327
|
+
const norm = normalizeImage({
|
|
6328
|
+
url: typeof b["url"] === "string" ? b["url"] : void 0,
|
|
6329
|
+
data: typeof b["data"] === "string" ? b["data"] : void 0,
|
|
6330
|
+
mime: typeof b["mime"] === "string" ? b["mime"] : void 0
|
|
6331
|
+
});
|
|
6332
|
+
return norm ? imageBlockForProvider(norm, provider) : null;
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
6335
|
+
return block;
|
|
6336
|
+
}
|
|
6337
|
+
/** Does a message's `content` array carry any vendor-neutral image block? */
|
|
6338
|
+
function contentHasNeutralImage(content) {
|
|
6339
|
+
if (!Array.isArray(content)) return false;
|
|
6340
|
+
return content.some((b) => {
|
|
6341
|
+
if (typeof b !== "object" || b === null) return false;
|
|
6342
|
+
const bb = b;
|
|
6343
|
+
return bb["type"] === "image" && ("url" in bb || "data" in bb) && !("source" in bb);
|
|
6344
|
+
});
|
|
6345
|
+
}
|
|
6346
|
+
/** Does ANY message carry a vendor-neutral image block in its content array? */
|
|
6347
|
+
function messagesHaveNeutralImage(messages) {
|
|
6348
|
+
return messages.some((m) => contentHasNeutralImage(m?.content));
|
|
6349
|
+
}
|
|
6350
|
+
/**
|
|
6351
|
+
* Normalize ONE message's content array into a single native-shaped block list:
|
|
6352
|
+
* a string → a `[{type:'text', text}]` block (empty string → no block); an
|
|
6353
|
+
* array → its blocks mapped via `nativeContentBlock` (vendor-neutral images to
|
|
6354
|
+
* native, others untouched, normalize-away blocks dropped). Returns the block
|
|
6355
|
+
* list for the array/string cases; for any other value returns `null` so the
|
|
6356
|
+
* caller can leave it untouched (don't coerce a shape we don't understand).
|
|
6357
|
+
*/
|
|
6358
|
+
function normalizeContentBlocks(content, provider) {
|
|
6359
|
+
if (typeof content === "string") return content.length > 0 ? [{
|
|
6360
|
+
type: "text",
|
|
6361
|
+
text: content
|
|
6362
|
+
}] : [];
|
|
6363
|
+
if (Array.isArray(content)) return content.map((b) => nativeContentBlock(b, provider)).filter((b) => b !== null);
|
|
6364
|
+
return null;
|
|
6365
|
+
}
|
|
6366
|
+
/**
|
|
6367
|
+
* Map a registered prompt's `messages` into a single provider-native format AND
|
|
6368
|
+
* (optionally) attach extra `images` to the last user message.
|
|
6369
|
+
*
|
|
6370
|
+
* `amba_ai_prompts_invoke` keeps its provider-shaped admin-test envelope for
|
|
6371
|
+
* backwards compatibility. Any VENDOR-NEUTRAL `{type:'image', url|data}` block
|
|
6372
|
+
* — whether it came
|
|
6373
|
+
* from the `images` convenience param OR was put directly in `messages` by the
|
|
6374
|
+
* caller — must be mapped to the provider's native shape HERE.
|
|
6375
|
+
*
|
|
6376
|
+
* Conservative by design: a message is rewritten ONLY when it needs it — it
|
|
6377
|
+
* carries vendor-neutral image blocks, OR it's the last user message receiving
|
|
6378
|
+
* appended `images`. Plain text / already-native messages pass through
|
|
6379
|
+
* byte-identical (no needless string→block conversion). When no user message
|
|
6380
|
+
* exists but extra `images` were supplied, a fresh user message is appended.
|
|
6381
|
+
* Returns a NEW array — the caller's `messages` is never mutated.
|
|
6382
|
+
*/
|
|
6383
|
+
function attachImagesToMessages(messages, images, provider) {
|
|
6384
|
+
const imageBlocks = images.map(normalizeImage).filter((img) => img !== null).map((img) => imageBlockForProvider(img, provider));
|
|
6385
|
+
const out = messages.map((m) => ({ ...m }));
|
|
6386
|
+
let lastUserIdx = -1;
|
|
6387
|
+
for (let i = out.length - 1; i >= 0; i--) if (out[i].role === "user") {
|
|
6388
|
+
lastUserIdx = i;
|
|
6389
|
+
break;
|
|
6390
|
+
}
|
|
6391
|
+
for (let i = 0; i < out.length; i++) {
|
|
6392
|
+
const isTarget = i === lastUserIdx && imageBlocks.length > 0;
|
|
6393
|
+
const needsImageMapping = contentHasNeutralImage(out[i].content);
|
|
6394
|
+
if (!isTarget && !needsImageMapping) continue;
|
|
6395
|
+
const blocks = normalizeContentBlocks(out[i].content, provider);
|
|
6396
|
+
if (blocks === null) {
|
|
6397
|
+
if (isTarget) out[i] = {
|
|
6398
|
+
...out[i],
|
|
6399
|
+
content: [{
|
|
6400
|
+
type: "text",
|
|
6401
|
+
text: JSON.stringify(out[i].content ?? "")
|
|
6402
|
+
}, ...imageBlocks]
|
|
6403
|
+
};
|
|
6404
|
+
continue;
|
|
6405
|
+
}
|
|
6406
|
+
const withImages = isTarget ? [...blocks, ...imageBlocks] : blocks;
|
|
6407
|
+
if (withImages.length === 0) continue;
|
|
6408
|
+
out[i] = {
|
|
6409
|
+
...out[i],
|
|
6410
|
+
content: withImages
|
|
6411
|
+
};
|
|
6412
|
+
}
|
|
6413
|
+
if (lastUserIdx === -1 && imageBlocks.length > 0) out.push({
|
|
6414
|
+
role: "user",
|
|
6415
|
+
content: imageBlocks
|
|
6416
|
+
});
|
|
6417
|
+
return out;
|
|
6418
|
+
}
|
|
6419
|
+
/**
|
|
6420
|
+
* Map ONE message's content into Gemini `parts`. A string → a single
|
|
6421
|
+
* `{text}` part; a non-string, non-array value → one JSON-stringified `{text}`
|
|
6422
|
+
* part; an array → each block mapped (vendor-neutral text → `{text}`,
|
|
6423
|
+
* vendor-neutral image → `inline_data`/`file_data`, a normalize-away image
|
|
6424
|
+
* dropped, and any UNKNOWN/alien block — e.g. an OpenAI `image_url` or
|
|
6425
|
+
* Anthropic `source` shape — dropped). Mirrors the gateway's
|
|
6426
|
+
* `mapContentGeminiParts` exactly so a server-side test invocation produces the
|
|
6427
|
+
* same upstream body the production `/prompts/:name/invoke` path does.
|
|
6428
|
+
*/
|
|
6429
|
+
function geminiPartsForContent(content) {
|
|
6430
|
+
if (typeof content === "string") return [{ text: content }];
|
|
6431
|
+
if (!Array.isArray(content)) return [{ text: JSON.stringify(content ?? "") }];
|
|
6432
|
+
const parts = [];
|
|
6433
|
+
for (const block of content) {
|
|
6434
|
+
if (typeof block !== "object" || block === null) continue;
|
|
6435
|
+
const b = block;
|
|
6436
|
+
if (b["type"] === "text" && typeof b["text"] === "string") {
|
|
6437
|
+
parts.push({ text: b["text"] });
|
|
6438
|
+
continue;
|
|
6439
|
+
}
|
|
6440
|
+
if (b["type"] === "image" && ("url" in b || "data" in b)) {
|
|
6441
|
+
const norm = normalizeImage({
|
|
6442
|
+
url: typeof b["url"] === "string" ? b["url"] : void 0,
|
|
6443
|
+
data: typeof b["data"] === "string" ? b["data"] : void 0,
|
|
6444
|
+
mime: typeof b["mime"] === "string" ? b["mime"] : void 0
|
|
6445
|
+
});
|
|
6446
|
+
if (norm) parts.push(imageBlockForProvider(norm, "gemini"));
|
|
6447
|
+
continue;
|
|
6448
|
+
}
|
|
6449
|
+
}
|
|
6450
|
+
return parts;
|
|
6451
|
+
}
|
|
6452
|
+
/**
|
|
6453
|
+
* Build Gemini's `contents[]` from a registered prompt's `messages`, mapping
|
|
6454
|
+
* roles (assistant/model → "model", everything else → "user"), translating
|
|
6455
|
+
* content into `parts`, and appending any extra `images` to the last user turn
|
|
6456
|
+
* (creating one if none exists). Messages whose content maps to NO parts are
|
|
6457
|
+
* dropped (an empty turn is rejected upstream). Returns a NEW array — the
|
|
6458
|
+
* caller's `messages` is never mutated.
|
|
6459
|
+
*/
|
|
6460
|
+
function geminiContentsFromMessages(messages, images) {
|
|
6461
|
+
const imageParts = images.map(normalizeImage).filter((img) => img !== null).map((img) => imageBlockForProvider(img, "gemini"));
|
|
6462
|
+
const contents = messages.map((m) => ({
|
|
6463
|
+
role: m.role === "assistant" || m.role === "model" ? "model" : "user",
|
|
6464
|
+
parts: geminiPartsForContent(m.content)
|
|
6465
|
+
}));
|
|
6466
|
+
if (imageParts.length > 0) {
|
|
6467
|
+
let lastUserIdx = -1;
|
|
6468
|
+
for (let i = contents.length - 1; i >= 0; i--) if (contents[i].role === "user") {
|
|
6469
|
+
lastUserIdx = i;
|
|
6470
|
+
break;
|
|
6471
|
+
}
|
|
6472
|
+
if (lastUserIdx === -1) contents.push({
|
|
6473
|
+
role: "user",
|
|
6474
|
+
parts: [...imageParts]
|
|
6475
|
+
});
|
|
6476
|
+
else contents[lastUserIdx] = {
|
|
6477
|
+
role: "user",
|
|
6478
|
+
parts: [...contents[lastUserIdx].parts, ...imageParts]
|
|
6479
|
+
};
|
|
6480
|
+
}
|
|
6481
|
+
return contents.filter((c) => c.parts.length > 0);
|
|
6482
|
+
}
|
|
6483
|
+
function registerTools$13(server, apiClient) {
|
|
6484
|
+
registerTool(server, apiClient, "amba_ai_prompts_create", [
|
|
6485
|
+
"Register a new AI prompt template on a project. Returns the persisted prompt row (name, version, provider, model, client_invokable).",
|
|
6486
|
+
"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\").",
|
|
6487
|
+
"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.",
|
|
6488
|
+
"Re-issuing this tool with an existing `name` will bump the prompt to a new version; prefer `amba_ai_prompts_update` for that intent."
|
|
6489
|
+
].join(" "), {
|
|
6490
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6491
|
+
name: z.string().describe("Prompt name. /^[a-z][a-z0-9_-]{0,127}$/."),
|
|
6492
|
+
provider: PROVIDER_ENUM$1.describe("AI provider. Must already be registered via `amba_ai_providers_set`."),
|
|
6493
|
+
model: z.string().describe("Provider-native model id."),
|
|
6494
|
+
system_prompt: z.string().nullable().optional().describe("System prompt sent on every invocation. Omit or null for no system prompt."),
|
|
6495
|
+
max_tokens: z.number().int().min(1).max(2e5).optional().describe("Max output tokens per invocation. Defaults to 4096."),
|
|
6496
|
+
rate_limit: RATE_LIMIT_SHAPE.nullable().optional().describe("Optional per-key rate limit."),
|
|
5730
6497
|
client_invokable: z.boolean().optional().describe("When true, end-user SDK code can invoke this prompt. Defaults false.")
|
|
5731
6498
|
}, async ({ project_id, name, provider, model, system_prompt, max_tokens, rate_limit, client_invokable }, { pat }) => {
|
|
5732
6499
|
const body = {
|
|
@@ -5780,9 +6547,10 @@ function registerTools$7(server, apiClient) {
|
|
|
5780
6547
|
});
|
|
5781
6548
|
}, ["amba_delete_ai_prompt"]);
|
|
5782
6549
|
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
|
|
5785
|
-
"
|
|
6550
|
+
"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.",
|
|
6551
|
+
"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). This server/admin path works even when `client_invokable=false` and enforces the named prompt budget, rate limit, and usage attribution.",
|
|
6552
|
+
"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.",
|
|
6553
|
+
"`extra_body` lets you pass allowlisted provider-native tuning parameters (temperature, top_p, response_format, …). The registered prompt model and configured max-token cap remain authoritative."
|
|
5786
6554
|
].join(" "), {
|
|
5787
6555
|
project_id: z.string().describe("The Amba project ID."),
|
|
5788
6556
|
name: z.string().describe("Registered prompt name."),
|
|
@@ -5790,21 +6558,39 @@ function registerTools$7(server, apiClient) {
|
|
|
5790
6558
|
role: z.string().describe("Anthropic / OpenAI role (e.g. \"user\", \"assistant\", \"system\")."),
|
|
5791
6559
|
content: z.unknown().describe("Message content. String or provider-native array form.")
|
|
5792
6560
|
})).min(1).describe("Provider-shaped messages array. At least one message required."),
|
|
6561
|
+
images: z.array(z.object({
|
|
6562
|
+
url: z.string().optional().describe("Remote image URL. Use this OR `data` (base64), not both."),
|
|
6563
|
+
data: z.string().optional().describe("Base64-encoded image bytes (no data: prefix)."),
|
|
6564
|
+
mime: z.string().optional().describe("MIME type for a base64 image (e.g. \"image/png\"). Defaults to image/png.")
|
|
6565
|
+
})).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
6566
|
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 }) => {
|
|
6567
|
+
}, async ({ project_id, name, messages, images, extra_body }, { pat }) => {
|
|
5795
6568
|
const prompt = (await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/ai/prompts/${encodeURIComponent(name)}`) ?? {}).data;
|
|
5796
6569
|
if (!prompt) throw new Error(`Prompt "${name}" not found.`);
|
|
5797
|
-
const
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
6570
|
+
const hasImagesParam = Array.isArray(images) && images.length > 0;
|
|
6571
|
+
let body;
|
|
6572
|
+
if (prompt.provider === "gemini") {
|
|
6573
|
+
body = {
|
|
6574
|
+
model: prompt.model,
|
|
6575
|
+
contents: geminiContentsFromMessages(messages, images ?? []),
|
|
6576
|
+
generationConfig: { maxOutputTokens: prompt.max_tokens }
|
|
6577
|
+
};
|
|
6578
|
+
if (prompt.system_prompt) body.systemInstruction = { parts: [{ text: prompt.system_prompt }] };
|
|
6579
|
+
} else {
|
|
6580
|
+
const messagesHaveImageBlocks = messagesHaveNeutralImage(messages);
|
|
6581
|
+
const effectiveMessages = hasImagesParam || messagesHaveImageBlocks ? attachImagesToMessages(messages, images ?? [], prompt.provider) : messages;
|
|
6582
|
+
body = {
|
|
6583
|
+
model: prompt.model,
|
|
6584
|
+
max_tokens: prompt.max_tokens
|
|
6585
|
+
};
|
|
6586
|
+
if (prompt.provider === "anthropic") {
|
|
6587
|
+
if (prompt.system_prompt) body.system = prompt.system_prompt;
|
|
6588
|
+
body.messages = effectiveMessages;
|
|
6589
|
+
} else body.messages = prompt.system_prompt ? [{
|
|
6590
|
+
role: "system",
|
|
6591
|
+
content: prompt.system_prompt
|
|
6592
|
+
}, ...effectiveMessages] : effectiveMessages;
|
|
6593
|
+
}
|
|
5808
6594
|
if (extra_body) {
|
|
5809
6595
|
const ALLOWED_EXTRA_KEYS = new Set([
|
|
5810
6596
|
"temperature",
|
|
@@ -5820,16 +6606,67 @@ function registerTools$7(server, apiClient) {
|
|
|
5820
6606
|
"tool_choice",
|
|
5821
6607
|
"metadata"
|
|
5822
6608
|
]);
|
|
5823
|
-
for (const
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
6609
|
+
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(", ")}`);
|
|
6610
|
+
if (prompt.provider === "gemini") {
|
|
6611
|
+
const gc = body.generationConfig ?? {};
|
|
6612
|
+
const GEMINI_KEY = {
|
|
6613
|
+
temperature: "temperature",
|
|
6614
|
+
top_p: "topP",
|
|
6615
|
+
top_k: "topK",
|
|
6616
|
+
seed: "seed",
|
|
6617
|
+
frequency_penalty: "frequencyPenalty",
|
|
6618
|
+
presence_penalty: "presencePenalty"
|
|
6619
|
+
};
|
|
6620
|
+
for (const [k, v] of Object.entries(extra_body)) if (k === "stop" || k === "stop_sequences") gc.stopSequences = Array.isArray(v) ? v : [v];
|
|
6621
|
+
else if (k === "response_format") {
|
|
6622
|
+
const rf = typeof v === "object" && v !== null ? v : null;
|
|
6623
|
+
const rfType = rf?.["type"];
|
|
6624
|
+
if (rfType === "json_object") gc.responseMimeType = "application/json";
|
|
6625
|
+
else if (rfType === "json_schema") {
|
|
6626
|
+
gc.responseMimeType = "application/json";
|
|
6627
|
+
const schema = rf && typeof rf["schema"] === "object" && rf["schema"] !== null ? rf["schema"] : void 0;
|
|
6628
|
+
if (schema !== void 0) gc.responseSchema = schema;
|
|
6629
|
+
}
|
|
6630
|
+
} else if (k in GEMINI_KEY) gc[GEMINI_KEY[k]] = v;
|
|
6631
|
+
else body[k] = v;
|
|
6632
|
+
body.generationConfig = gc;
|
|
6633
|
+
} else for (const [k, v] of Object.entries(extra_body)) body[k] = v;
|
|
5827
6634
|
}
|
|
5828
|
-
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/ai/
|
|
6635
|
+
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/ai/prompts/${encodeURIComponent(name)}/invoke`, { body: {
|
|
5829
6636
|
provider: prompt.provider,
|
|
5830
6637
|
body
|
|
5831
6638
|
} }));
|
|
5832
6639
|
}, ["amba_invoke_ai_prompt"]);
|
|
6640
|
+
registerTool(server, apiClient, "amba_ai_prompts_set_budget", [
|
|
6641
|
+
"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.",
|
|
6642
|
+
"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).",
|
|
6643
|
+
"Returns the persisted budget. For the whole-project ceiling instead, use `amba_billing_set_ceiling`."
|
|
6644
|
+
].join(" "), {
|
|
6645
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6646
|
+
name: z.string().describe("Registered prompt name to budget."),
|
|
6647
|
+
budget_usd: z.number().min(0).max(1e6).nullable().describe("Per-period USD ceiling. Pass null to clear the budget (unlimited)."),
|
|
6648
|
+
budget_period: z.enum([
|
|
6649
|
+
"daily",
|
|
6650
|
+
"monthly",
|
|
6651
|
+
"total"
|
|
6652
|
+
]).optional().describe("Reset cadence. Defaults to \"monthly\" when a budget is set.")
|
|
6653
|
+
}, async ({ project_id, name, budget_usd, budget_period }, { pat }) => {
|
|
6654
|
+
const body = { budget_usd };
|
|
6655
|
+
if (budget_period !== void 0) body.budget_period = budget_period;
|
|
6656
|
+
return jsonResult(await callWithPat(apiClient, pat, "PUT", `/projects/${encodeURIComponent(project_id)}/ai/prompts/${encodeURIComponent(name)}/budget`, { body }));
|
|
6657
|
+
});
|
|
6658
|
+
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(" "), {
|
|
6659
|
+
project_id: z.string().describe("The Amba project ID."),
|
|
6660
|
+
name: z.string().describe("Registered prompt name."),
|
|
6661
|
+
period: z.enum([
|
|
6662
|
+
"daily",
|
|
6663
|
+
"monthly",
|
|
6664
|
+
"total"
|
|
6665
|
+
]).optional().describe("Spend window to report. Defaults to the prompt's configured budget period.")
|
|
6666
|
+
}, async ({ project_id, name, period }, { pat }) => {
|
|
6667
|
+
const query = period ? `?period=${encodeURIComponent(period)}` : "";
|
|
6668
|
+
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/ai/prompts/${encodeURIComponent(name)}/spend${query}`));
|
|
6669
|
+
});
|
|
5833
6670
|
}
|
|
5834
6671
|
//#endregion
|
|
5835
6672
|
//#region src/tools/ai-providers-admin.ts
|
|
@@ -5839,7 +6676,7 @@ const PROVIDER_ENUM = z.enum([
|
|
|
5839
6676
|
"mistral",
|
|
5840
6677
|
"gemini"
|
|
5841
6678
|
]);
|
|
5842
|
-
function registerTools$
|
|
6679
|
+
function registerTools$12(server, apiClient) {
|
|
5843
6680
|
registerTool(server, apiClient, "amba_ai_providers_set", [
|
|
5844
6681
|
"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
6682
|
"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 +6716,7 @@ const STATUS_ENUM = z.enum([
|
|
|
5879
6716
|
"paused",
|
|
5880
6717
|
"ended"
|
|
5881
6718
|
]).describe("active = assigns + exposes new users; paused = serves existing assignments but issues no new ones; ended = read-only.");
|
|
5882
|
-
function registerTools$
|
|
6719
|
+
function registerTools$11(server, apiClient) {
|
|
5883
6720
|
registerTool(server, apiClient, "amba_experiments_create", [
|
|
5884
6721
|
"Create an A/B experiment: a stable `key`, a display `name`, and >= 2 weighted variants.",
|
|
5885
6722
|
"End-users are bucketed into a STICKY variant the first time they request an assignment",
|
|
@@ -5982,25 +6819,28 @@ function registerTools$5(server, apiClient) {
|
|
|
5982
6819
|
}
|
|
5983
6820
|
//#endregion
|
|
5984
6821
|
//#region src/tools/promotion.ts
|
|
5985
|
-
function registerTools$
|
|
6822
|
+
function registerTools$10(server, apiClient) {
|
|
5986
6823
|
registerTool(server, apiClient, "amba_project_export", [
|
|
5987
6824
|
"Export a declarative bundle of a project's reusable configuration so it can be re-created in another project (e.g. promote dev → prod) in one operation.",
|
|
5988
|
-
"The bundle includes (each section only when present): remote configs, collection SCHEMAS (column defs + indexes, no rows), content libraries + their items,
|
|
5989
|
-
"It carries NO secrets, NO per-user data, and NO end-user rows. Pass the returned `bundle` to `amba_project_import` against the target project."
|
|
6825
|
+
"The bundle includes (each section only when present): remote configs, collection SCHEMAS (column defs + indexes, no rows), content libraries + their items, the full gamification set — currencies, achievements, streaks, challenges, leaderboards, and xp rules — as DEFINITIONS only, plus integrations (push providers et al.) as non-secret config: bundle id, team/key ids, environment travel; `.p8` keys, service-account JSON, and API keys are stripped and marked `credentials: \"stripped\"`.",
|
|
6826
|
+
"It carries NO secrets, NO per-user data, and NO end-user rows — safe to save to disk, commit, or diff. Pass the returned `bundle` to `amba_project_import` against the target project; if both projects belong to the same developer, import with `include_integration_credentials: true` and the credentials are copied server-side too, so push works on the target without re-uploading keys."
|
|
5990
6827
|
].join(" "), { project_id: z.string().describe("The Amba project ID to export configuration FROM.") }, async ({ project_id }, { pat }) => {
|
|
5991
6828
|
return jsonResult(await callWithPat(apiClient, pat, "GET", `/projects/${encodeURIComponent(project_id)}/promotion/export`));
|
|
5992
6829
|
});
|
|
5993
6830
|
registerTool(server, apiClient, "amba_project_import", [
|
|
5994
|
-
"Import a declarative configuration bundle (from `amba_project_export`) into a project. Idempotently creates remote configs, collections, content libraries + items,
|
|
5995
|
-
"mode=\"skip_existing\" (default) leaves any entity that already exists (by name/key/code) untouched; mode=\"merge\" refreshes the definition of existing entities. Either mode is safe to run twice — re-import never duplicates.",
|
|
5996
|
-
"
|
|
6831
|
+
"Import a declarative configuration bundle (from `amba_project_export`) into a project. Idempotently creates remote configs, collections, content libraries + items, gamification definitions (currencies, achievements, streaks, challenges, leaderboards, xp rules), and integrations.",
|
|
6832
|
+
"mode=\"skip_existing\" (default) leaves any entity that already exists (by name/key/code) untouched; mode=\"merge\" refreshes the definition of existing entities. Either mode is safe to run twice — re-import never duplicates, and merge never clobbers credentials an existing integration already has.",
|
|
6833
|
+
"Integrations: bundles never carry credential material. By default each imported integration lands inactive in a `pending_credentials` state and the response tells you exactly which call uploads the credentials (`amba_integrations_configure`). When the source and target projects belong to the SAME developer, pass `include_integration_credentials: true` instead — the stored credentials are copied server-side between the projects (never through this conversation) and the integrations activate immediately, making dev → prod push promotion a single call. Cross-developer copies are refused with a 403.",
|
|
6834
|
+
"Returns a per-section summary of { created, skipped, updated } plus a per-integration status list. Collections are create-if-absent only (an existing collection is skipped; alter the schema via the dedicated collection tools)."
|
|
5997
6835
|
].join(" "), {
|
|
5998
6836
|
project_id: z.string().describe("The Amba project ID to import configuration INTO."),
|
|
5999
6837
|
bundle: z.record(z.unknown()).describe("The bundle object returned by `amba_project_export` (its `data` field)."),
|
|
6000
|
-
mode: z.enum(["merge", "skip_existing"]).optional().describe("Conflict resolution. \"skip_existing\" (default) no-ops on a name/key/code conflict; \"merge\" updates the existing definition.")
|
|
6001
|
-
|
|
6838
|
+
mode: z.enum(["merge", "skip_existing"]).optional().describe("Conflict resolution. \"skip_existing\" (default) no-ops on a name/key/code conflict; \"merge\" updates the existing definition."),
|
|
6839
|
+
include_integration_credentials: z.boolean().optional().describe("Copy integration credentials (push keys, provider API keys) server-side from the bundle's source project and activate the integrations on the target. Only allowed when both projects belong to the same developer; otherwise the import is refused. Omit (default) to create integrations in a pending_credentials state and upload credentials separately via `amba_integrations_configure`.")
|
|
6840
|
+
}, async ({ project_id, bundle, mode, include_integration_credentials }, { pat }) => {
|
|
6002
6841
|
const body = { bundle };
|
|
6003
6842
|
if (mode !== void 0) body.mode = mode;
|
|
6843
|
+
if (include_integration_credentials !== void 0) body.include_integration_credentials = include_integration_credentials;
|
|
6004
6844
|
return jsonResult(await callWithPat(apiClient, pat, "POST", `/projects/${encodeURIComponent(project_id)}/promotion/import`, { body }));
|
|
6005
6845
|
});
|
|
6006
6846
|
}
|
|
@@ -6080,7 +6920,11 @@ const TIER_CATALOG = {
|
|
|
6080
6920
|
unit_description: "$0.50 per 10K push"
|
|
6081
6921
|
},
|
|
6082
6922
|
db_storage_per_gb_month_usd: 1.5,
|
|
6083
|
-
media_storage_per_gb_month_usd: .1
|
|
6923
|
+
media_storage_per_gb_month_usd: .1,
|
|
6924
|
+
app_mcp_tool_calls: {
|
|
6925
|
+
per_unit_usd: .001,
|
|
6926
|
+
unit_description: "$1.00 per 1K agent tool calls"
|
|
6927
|
+
}
|
|
6084
6928
|
},
|
|
6085
6929
|
telemetry_event_overage: {
|
|
6086
6930
|
per_unit_usd: 1e-7,
|
|
@@ -6088,8 +6932,8 @@ const TIER_CATALOG = {
|
|
|
6088
6932
|
note: "Use Amba.track(name, props, { telemetry: true }) for high-volume telemetry. Cheaper, no fan-out to segments/workflows/push."
|
|
6089
6933
|
}
|
|
6090
6934
|
};
|
|
6091
|
-
function registerTools$
|
|
6092
|
-
registerTool(server, apiClient, "amba_billing_status", "Get the
|
|
6935
|
+
function registerTools$9(server, apiClient) {
|
|
6936
|
+
registerTool(server, apiClient, "amba_billing_status", "Get the persisted billing `tier`, runtime `effective_tier` (use this for quota/feature decisions), comp status, current-period usage by meter (MAU, events, push, db_storage, media_storage, agent tool calls) with per-meter cost, the billing period and its reset date, spend ceiling + percent consumed, a projected end-of-period overage, the live enforcement state (whether the project is in read-only mode — metered writes returning 402 — and which meters are over quota), 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
6937
|
const result = await client.get(`/projects/${project_id}/billing/status`);
|
|
6094
6938
|
return { content: [{
|
|
6095
6939
|
type: "text",
|
|
@@ -6102,7 +6946,7 @@ function registerTools$3(server, apiClient) {
|
|
|
6102
6946
|
text: JSON.stringify(TIER_CATALOG, null, 2)
|
|
6103
6947
|
}] };
|
|
6104
6948
|
});
|
|
6105
|
-
registerTool(server, apiClient, "amba_billing_set_ceiling", "Set or remove the monthly spend ceiling for a project. When
|
|
6949
|
+
registerTool(server, apiClient, "amba_billing_set_ceiling", "Set or remove the monthly spend ceiling for a project. When the current billing period's overage cost reaches the ceiling, the project degrades to read-only mode: metered WRITE operations (tracking events, inserting collection rows, uploads, push sends, new signups) return 402 with a machine-readable payload (code, current usage, ceiling, reset date) while reads keep working. Control-plane webhook events `billing.ceiling_warning` / `billing.ceiling_reached` fire once per period at 80% / 100% so you can alert. The new ceiling takes effect immediately. Pass ceiling_usd: null to remove the ceiling (linear overage with no cap). This is a write action — surface to the human before calling.", {
|
|
6106
6950
|
project_id: z.string().describe("The project ID"),
|
|
6107
6951
|
ceiling_usd: z.number().min(0).max(1e5).nullable().describe("Maximum monthly bill in USD, or null to disable")
|
|
6108
6952
|
}, async ({ project_id, ceiling_usd }, { client }) => {
|
|
@@ -6114,8 +6958,120 @@ function registerTools$3(server, apiClient) {
|
|
|
6114
6958
|
});
|
|
6115
6959
|
}
|
|
6116
6960
|
//#endregion
|
|
6961
|
+
//#region src/tools/payments.ts
|
|
6962
|
+
function registerTools$8(server, apiClient) {
|
|
6963
|
+
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.", {
|
|
6964
|
+
project_id: z.string().describe("The project ID"),
|
|
6965
|
+
country: z.string().length(2).optional().describe("Two-letter ISO country code for the seller (e.g. \"US\")"),
|
|
6966
|
+
email: z.string().optional().describe("Contact email for the connected account")
|
|
6967
|
+
}, async ({ project_id, country, email }, { client }) => {
|
|
6968
|
+
const result = await client.post(`/projects/${project_id}/payments/accounts`, {
|
|
6969
|
+
...country ? { country } : {},
|
|
6970
|
+
...email ? { email } : {}
|
|
6971
|
+
});
|
|
6972
|
+
return { content: [{
|
|
6973
|
+
type: "text",
|
|
6974
|
+
text: JSON.stringify(result, null, 2)
|
|
6975
|
+
}] };
|
|
6976
|
+
});
|
|
6977
|
+
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 }) => {
|
|
6978
|
+
const result = await client.post(`/projects/${project_id}/payments/accounts/link`, {});
|
|
6979
|
+
return { content: [{
|
|
6980
|
+
type: "text",
|
|
6981
|
+
text: JSON.stringify(result, null, 2)
|
|
6982
|
+
}] };
|
|
6983
|
+
});
|
|
6984
|
+
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 }) => {
|
|
6985
|
+
const result = await client.get(`/projects/${project_id}/payments/accounts`);
|
|
6986
|
+
return { content: [{
|
|
6987
|
+
type: "text",
|
|
6988
|
+
text: JSON.stringify(result, null, 2)
|
|
6989
|
+
}] };
|
|
6990
|
+
});
|
|
6991
|
+
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.", {
|
|
6992
|
+
project_id: z.string().describe("The project ID"),
|
|
6993
|
+
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")
|
|
6994
|
+
}, async ({ project_id, default_platform_fee_bps }, { client }) => {
|
|
6995
|
+
const result = await client.put(`/projects/${project_id}/payments/config`, { default_platform_fee_bps });
|
|
6996
|
+
return { content: [{
|
|
6997
|
+
type: "text",
|
|
6998
|
+
text: JSON.stringify(result, null, 2)
|
|
6999
|
+
}] };
|
|
7000
|
+
});
|
|
7001
|
+
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.", {
|
|
7002
|
+
project_id: z.string().describe("The project ID"),
|
|
7003
|
+
amount: z.number().int().positive().describe("Amount in smallest currency unit (cents)"),
|
|
7004
|
+
currency: z.string().length(3).describe("3-letter ISO currency code (lowercase, e.g. \"usd\")"),
|
|
7005
|
+
application_fee_amount: z.number().int().min(0).optional().describe("Platform fee in cents (overrides bps/default)"),
|
|
7006
|
+
platform_fee_bps: z.number().int().min(0).max(1e4).optional().describe("Platform fee in basis points (250 = 2.5%)"),
|
|
7007
|
+
client_reference: z.string().optional().describe("Opaque reference (e.g. your order id) stored with the payment")
|
|
7008
|
+
}, async ({ project_id, ...body }, { client }) => {
|
|
7009
|
+
const result = await client.post(`/projects/${project_id}/payments/charges`, body);
|
|
7010
|
+
return { content: [{
|
|
7011
|
+
type: "text",
|
|
7012
|
+
text: JSON.stringify(result, null, 2)
|
|
7013
|
+
}] };
|
|
7014
|
+
});
|
|
7015
|
+
registerTool(server, apiClient, "amba_payments_refund", "Refund a payment, in full or in part. Pass the payment id (returned by amba_payments_charge) and optionally an amount in the smallest currency unit (cents) for a partial refund — omit amount to refund the full charge. The customer is refunded and BOTH the platform fee and the seller transfer are reversed proportionally, so neither Amba nor the seller is left funding it. Money-moving and not reversible — surface to the human before calling.", {
|
|
7016
|
+
project_id: z.string().describe("The project ID"),
|
|
7017
|
+
payment_id: z.string().describe("The payment id to refund (returned by amba_payments_charge)"),
|
|
7018
|
+
amount: z.number().int().positive().optional().describe("Partial refund amount in cents; omit to refund the full charge"),
|
|
7019
|
+
reason: z.enum([
|
|
7020
|
+
"duplicate",
|
|
7021
|
+
"fraudulent",
|
|
7022
|
+
"requested_by_customer"
|
|
7023
|
+
]).optional().describe("Optional refund reason"),
|
|
7024
|
+
reference: z.string().optional().describe("Opaque idempotency reference: pass the same value to make a retry safe (returns the same refund); omit it and each call issues a new refund")
|
|
7025
|
+
}, async ({ project_id, payment_id, ...body }, { client }) => {
|
|
7026
|
+
const result = await client.post(`/projects/${project_id}/payments/charges/${payment_id}/refund`, body);
|
|
7027
|
+
return { content: [{
|
|
7028
|
+
type: "text",
|
|
7029
|
+
text: JSON.stringify(result, null, 2)
|
|
7030
|
+
}] };
|
|
7031
|
+
});
|
|
7032
|
+
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 }) => {
|
|
7033
|
+
const result = await client.get(`/projects/${project_id}/payments/balance`);
|
|
7034
|
+
return { content: [{
|
|
7035
|
+
type: "text",
|
|
7036
|
+
text: JSON.stringify(result, null, 2)
|
|
7037
|
+
}] };
|
|
7038
|
+
});
|
|
7039
|
+
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.", {
|
|
7040
|
+
project_id: z.string().describe("The project ID"),
|
|
7041
|
+
limit: z.number().int().min(1).max(100).optional().describe("How many payouts to return (default 10)")
|
|
7042
|
+
}, async ({ project_id, limit }, { client }) => {
|
|
7043
|
+
const qs = limit ? `?limit=${limit}` : "";
|
|
7044
|
+
const result = await client.get(`/projects/${project_id}/payments/payouts${qs}`);
|
|
7045
|
+
return { content: [{
|
|
7046
|
+
type: "text",
|
|
7047
|
+
text: JSON.stringify(result, null, 2)
|
|
7048
|
+
}] };
|
|
7049
|
+
});
|
|
7050
|
+
}
|
|
7051
|
+
//#endregion
|
|
7052
|
+
//#region src/tools/agent-checkout.ts
|
|
7053
|
+
function registerTools$7(server, apiClient) {
|
|
7054
|
+
registerTool(server, apiClient, "amba_agent_checkout_enable", "Turn agent checkout on (or off) for a project, so your users' AI agents can pay inside your app and have entitlements unlock in the same motion. Default-deny: it stays off until you enable it. Requires a connected payments account first (set one up with amba_payments_account_create). Returns the per-rail capabilities so you can see which payment methods the account can settle. Money-affecting setting — surface it to the human before calling.", {
|
|
7055
|
+
project_id: z.string().describe("The project ID"),
|
|
7056
|
+
enabled: z.boolean().describe("true to enable agent checkout, false to disable")
|
|
7057
|
+
}, async ({ project_id, enabled }, { client }) => {
|
|
7058
|
+
const result = await client.post(`/projects/${project_id}/agent-checkout/enable`, { enabled });
|
|
7059
|
+
return { content: [{
|
|
7060
|
+
type: "text",
|
|
7061
|
+
text: JSON.stringify(result, null, 2)
|
|
7062
|
+
}] };
|
|
7063
|
+
});
|
|
7064
|
+
registerTool(server, apiClient, "amba_agent_checkout_capabilities", "Read which agent-checkout settlement rails a project can offer (and, when a rail is unavailable, exactly why). Use this before enabling agent checkout, or to confirm a project is ready to accept agent-initiated payments. Returns enabled plus a per-rail availability list.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
7065
|
+
const result = await client.get(`/projects/${project_id}/agent-checkout/capabilities`);
|
|
7066
|
+
return { content: [{
|
|
7067
|
+
type: "text",
|
|
7068
|
+
text: JSON.stringify(result, null, 2)
|
|
7069
|
+
}] };
|
|
7070
|
+
});
|
|
7071
|
+
}
|
|
7072
|
+
//#endregion
|
|
6117
7073
|
//#region src/tools/invites.ts
|
|
6118
|
-
function registerTools$
|
|
7074
|
+
function registerTools$6(server, apiClient) {
|
|
6119
7075
|
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).", {
|
|
6120
7076
|
project_id: z.string().describe("The project ID"),
|
|
6121
7077
|
email: z.string().describe("Teammate's email address"),
|
|
@@ -6164,8 +7120,519 @@ function registerTools$2(server, apiClient) {
|
|
|
6164
7120
|
});
|
|
6165
7121
|
}
|
|
6166
7122
|
//#endregion
|
|
7123
|
+
//#region src/tools/orgs.ts
|
|
7124
|
+
/**
|
|
7125
|
+
* Organization tools (migration 049). Orgs are the ownership + payment-entity
|
|
7126
|
+
* boundary; affiliates and app-factory child projects are all orgs. These wrap
|
|
7127
|
+
* the `/v1/admin/orgs*` and `/v1/admin/provision` routes.
|
|
7128
|
+
*/
|
|
7129
|
+
function registerTools$5(server, apiClient) {
|
|
7130
|
+
registerTool(server, apiClient, "amba_orgs_create", "Create an organization. Optionally nest it under a parent org (parent_org_id) — a child org can bill to its parent (payment_source=\"parent\") or hold its own card (payment_source=\"self\").", {
|
|
7131
|
+
name: z.string().describe("Human-readable org name"),
|
|
7132
|
+
parent_org_id: z.string().optional().describe("Parent org UUID to nest under (omit for a top-level org)"),
|
|
7133
|
+
payment_source: z.enum(["self", "parent"]).optional().describe("\"self\" = bill this org's own card; \"parent\" = bill the nearest self-paying ancestor. Default \"self\"."),
|
|
7134
|
+
slug: z.string().optional().describe("Optional unique URL slug (lowercase alphanumeric/hyphen)"),
|
|
7135
|
+
external_ref: z.string().optional().describe("Caller-stable idempotency key (unique per parent)")
|
|
7136
|
+
}, async ({ name, parent_org_id, payment_source, slug, external_ref }, { client }) => {
|
|
7137
|
+
const body = { name };
|
|
7138
|
+
if (parent_org_id !== void 0) body["parent_org_id"] = parent_org_id;
|
|
7139
|
+
if (payment_source !== void 0) body["payment_source"] = payment_source;
|
|
7140
|
+
if (slug !== void 0) body["slug"] = slug;
|
|
7141
|
+
if (external_ref !== void 0) body["external_ref"] = external_ref;
|
|
7142
|
+
const result = await client.post("/orgs", body);
|
|
7143
|
+
return { content: [{
|
|
7144
|
+
type: "text",
|
|
7145
|
+
text: JSON.stringify(result, null, 2)
|
|
7146
|
+
}] };
|
|
7147
|
+
});
|
|
7148
|
+
registerTool(server, apiClient, "amba_orgs_list", "List organizations the authenticated developer can access, including descendant orgs in the hierarchy.", {}, async (_, { client }) => {
|
|
7149
|
+
const result = await client.get("/orgs");
|
|
7150
|
+
return { content: [{
|
|
7151
|
+
type: "text",
|
|
7152
|
+
text: JSON.stringify(result, null, 2)
|
|
7153
|
+
}] };
|
|
7154
|
+
});
|
|
7155
|
+
registerTool(server, apiClient, "amba_orgs_get", "Get an organization by id, including its child-org and project counts and your effective role.", { org_id: z.string().describe("The org UUID") }, async ({ org_id }, { client }) => {
|
|
7156
|
+
const result = await client.get(`/orgs/${org_id}`);
|
|
7157
|
+
return { content: [{
|
|
7158
|
+
type: "text",
|
|
7159
|
+
text: JSON.stringify(result, null, 2)
|
|
7160
|
+
}] };
|
|
7161
|
+
});
|
|
7162
|
+
registerTool(server, apiClient, "amba_orgs_update", "Update an organization: name, slug, or payment_source. Flipping payment_source to \"self\" is the first step of graduating a child org to bill directly.", {
|
|
7163
|
+
org_id: z.string().describe("The org UUID"),
|
|
7164
|
+
name: z.string().optional().describe("New name"),
|
|
7165
|
+
slug: z.string().optional().describe("New slug"),
|
|
7166
|
+
payment_source: z.enum(["self", "parent"]).optional().describe("Billing source toggle")
|
|
7167
|
+
}, async ({ org_id, name, slug, payment_source }, { client }) => {
|
|
7168
|
+
const body = {};
|
|
7169
|
+
if (name !== void 0) body["name"] = name;
|
|
7170
|
+
if (slug !== void 0) body["slug"] = slug;
|
|
7171
|
+
if (payment_source !== void 0) body["payment_source"] = payment_source;
|
|
7172
|
+
const result = await client.patch(`/orgs/${org_id}`, body);
|
|
7173
|
+
return { content: [{
|
|
7174
|
+
type: "text",
|
|
7175
|
+
text: JSON.stringify(result, null, 2)
|
|
7176
|
+
}] };
|
|
7177
|
+
});
|
|
7178
|
+
registerTool(server, apiClient, "amba_orgs_invite_member", "Invite a developer to an organization by email. Returns an invite URL the recipient accepts to join with the given role.", {
|
|
7179
|
+
org_id: z.string().describe("The org UUID"),
|
|
7180
|
+
email: z.string().describe("Invitee email"),
|
|
7181
|
+
role: z.enum([
|
|
7182
|
+
"admin",
|
|
7183
|
+
"member",
|
|
7184
|
+
"viewer"
|
|
7185
|
+
]).optional().describe("Role to grant on accept (default member)"),
|
|
7186
|
+
expires_in_days: z.number().int().optional().describe("Invite expiry in days (1–30, default 7)")
|
|
7187
|
+
}, async ({ org_id, email, role, expires_in_days }, { client }) => {
|
|
7188
|
+
const body = { email };
|
|
7189
|
+
if (role !== void 0) body["role"] = role;
|
|
7190
|
+
if (expires_in_days !== void 0) body["expires_in_days"] = expires_in_days;
|
|
7191
|
+
const result = await client.post(`/orgs/${org_id}/invites`, body);
|
|
7192
|
+
return { content: [{
|
|
7193
|
+
type: "text",
|
|
7194
|
+
text: JSON.stringify(result, null, 2)
|
|
7195
|
+
}] };
|
|
7196
|
+
});
|
|
7197
|
+
registerTool(server, apiClient, "amba_orgs_transfer_owner", "Transfer ownership of an organization to another developer (who must already be reachable). Optionally remove the previous owner instead of demoting them to admin.", {
|
|
7198
|
+
org_id: z.string().describe("The org UUID"),
|
|
7199
|
+
new_owner_developer_id: z.string().describe("Developer UUID of the new owner"),
|
|
7200
|
+
remove_previous_owner: z.boolean().optional().describe("If true, remove the previous owner entirely instead of demoting to admin")
|
|
7201
|
+
}, async ({ org_id, new_owner_developer_id, remove_previous_owner }, { client }) => {
|
|
7202
|
+
const body = { new_owner_developer_id };
|
|
7203
|
+
if (remove_previous_owner !== void 0) body["remove_previous_owner"] = remove_previous_owner;
|
|
7204
|
+
const result = await client.post(`/orgs/${org_id}/transfer-ownership`, body);
|
|
7205
|
+
return { content: [{
|
|
7206
|
+
type: "text",
|
|
7207
|
+
text: JSON.stringify(result, null, 2)
|
|
7208
|
+
}] };
|
|
7209
|
+
});
|
|
7210
|
+
registerTool(server, apiClient, "amba_provision_app", "App factory: atomically create a child org + a project under it and return the project keys. IDEMPOTENT by external_ref — a retry with the same (parent_org_id, external_ref) returns the same org/project (with keys_already_issued=true, since plaintext keys are shown only once). Store the keys from the first response.", {
|
|
7211
|
+
parent_org_id: z.string().describe("Parent org UUID to create the child under"),
|
|
7212
|
+
external_ref: z.string().describe("Stable per-app key for idempotency (e.g. your customer/app id)"),
|
|
7213
|
+
org_name: z.string().optional().describe("Child org display name (defaults to external_ref)"),
|
|
7214
|
+
project_name: z.string().optional().describe("Project display name (defaults to org_name)"),
|
|
7215
|
+
bundle_id: z.string().optional().describe("Apple bundle id / Sign in with Apple audience"),
|
|
7216
|
+
google_oauth_client_id: z.string().optional().describe("Google Sign In audience client id"),
|
|
7217
|
+
platform: z.enum([
|
|
7218
|
+
"ios",
|
|
7219
|
+
"android",
|
|
7220
|
+
"all"
|
|
7221
|
+
]).optional().describe("Target platform (default all)"),
|
|
7222
|
+
billing_mode: z.enum(["self", "parent"]).optional().describe("Child org payment_source: \"parent\" (default — bill the parent) or \"self\"")
|
|
7223
|
+
}, async (args, { client }) => {
|
|
7224
|
+
const body = {
|
|
7225
|
+
parent_org_id: args.parent_org_id,
|
|
7226
|
+
external_ref: args.external_ref
|
|
7227
|
+
};
|
|
7228
|
+
for (const k of [
|
|
7229
|
+
"org_name",
|
|
7230
|
+
"project_name",
|
|
7231
|
+
"bundle_id",
|
|
7232
|
+
"google_oauth_client_id",
|
|
7233
|
+
"platform",
|
|
7234
|
+
"billing_mode"
|
|
7235
|
+
]) if (args[k] !== void 0) body[k] = args[k];
|
|
7236
|
+
const result = await client.post("/provision", body);
|
|
7237
|
+
return { content: [{
|
|
7238
|
+
type: "text",
|
|
7239
|
+
text: JSON.stringify(result, null, 2)
|
|
7240
|
+
}] };
|
|
7241
|
+
});
|
|
7242
|
+
}
|
|
7243
|
+
//#endregion
|
|
7244
|
+
//#region src/tools/service-accounts.ts
|
|
7245
|
+
/**
|
|
7246
|
+
* Service-account lifecycle tools for Fork C delegated operator tokens.
|
|
7247
|
+
*
|
|
7248
|
+
* These tools intentionally cover only SA lifecycle. Delegated token minting is
|
|
7249
|
+
* an HTTP backend-to-backend call made by the partner app with the `amb_dsvc_`
|
|
7250
|
+
* secret; it is not exposed as an MCP tool.
|
|
7251
|
+
*/
|
|
7252
|
+
function registerTools$4(server, apiClient) {
|
|
7253
|
+
registerTool(server, apiClient, "amba_service_accounts_create", "Create a scoped service account under an organization. Returns the amb_dsvc_ secret once; store it in the partner backend.", {
|
|
7254
|
+
org_id: z.string().describe("Organization UUID that roots the service-account subtree"),
|
|
7255
|
+
name: z.string().describe("Human-readable service account name"),
|
|
7256
|
+
max_role: z.enum([
|
|
7257
|
+
"owner",
|
|
7258
|
+
"admin",
|
|
7259
|
+
"member",
|
|
7260
|
+
"viewer"
|
|
7261
|
+
]).describe("Maximum role this service account may delegate"),
|
|
7262
|
+
acting_developer_id: z.string().optional().describe("Optional developer UUID stamped as ownership lineage for delegated provisioning. Must be owner/admin on the org.")
|
|
7263
|
+
}, async ({ org_id, name, max_role, acting_developer_id }, { client }) => {
|
|
7264
|
+
const body = {
|
|
7265
|
+
name,
|
|
7266
|
+
max_role
|
|
7267
|
+
};
|
|
7268
|
+
if (acting_developer_id !== void 0) body["acting_developer_id"] = acting_developer_id;
|
|
7269
|
+
const result = await client.post(`/orgs/${org_id}/service-accounts`, body);
|
|
7270
|
+
return { content: [{
|
|
7271
|
+
type: "text",
|
|
7272
|
+
text: JSON.stringify(result, null, 2)
|
|
7273
|
+
}] };
|
|
7274
|
+
});
|
|
7275
|
+
registerTool(server, apiClient, "amba_service_accounts_list", "List service accounts for an organization. Secrets are redacted; only prefix/last-4 and lifecycle metadata are returned.", { org_id: z.string().describe("Organization UUID") }, async ({ org_id }, { client }) => {
|
|
7276
|
+
const result = await client.get(`/orgs/${org_id}/service-accounts`);
|
|
7277
|
+
return { content: [{
|
|
7278
|
+
type: "text",
|
|
7279
|
+
text: JSON.stringify(result, null, 2)
|
|
7280
|
+
}] };
|
|
7281
|
+
});
|
|
7282
|
+
registerTool(server, apiClient, "amba_service_accounts_revoke", "Soft-revoke a service account. Its delegated tokens become invalid after the auth cache TTL.", { service_account_id: z.string().describe("Service account UUID") }, async ({ service_account_id }, { client }) => {
|
|
7283
|
+
const result = await client.post(`/service-accounts/${service_account_id}/revoke`);
|
|
7284
|
+
return { content: [{
|
|
7285
|
+
type: "text",
|
|
7286
|
+
text: JSON.stringify(result, null, 2)
|
|
7287
|
+
}] };
|
|
7288
|
+
});
|
|
7289
|
+
}
|
|
7290
|
+
//#endregion
|
|
7291
|
+
//#region src/tools/affiliate.ts
|
|
7292
|
+
/**
|
|
7293
|
+
* Affiliate Programs tools (migration 052). Two personas, structurally scoped:
|
|
7294
|
+
* - `amba_affiliate_*` — the program owner (admin); operates over the whole
|
|
7295
|
+
* program. PAT must manage the program's owner org.
|
|
7296
|
+
* - `amba_affiliate_my_*` — the affiliate (self-service); derives the
|
|
7297
|
+
* affiliate from the PAT's own org(s) and takes NO other-affiliate id, so
|
|
7298
|
+
* an affiliate cannot read a peer's data.
|
|
7299
|
+
* Zero-to-enrolled bootstrap is `amba_affiliate_signup` (public, in auth.ts).
|
|
7300
|
+
*/
|
|
7301
|
+
function registerTools$3(server, apiClient) {
|
|
7302
|
+
registerTool(server, apiClient, "amba_affiliate_programs_create", "Create an affiliate program owned by an org. commission is a JSON tagged-union: {\"type\":\"percentage\",\"bps\":2000} | {\"type\":\"flat\",\"amount_cents\":5000} | {\"type\":\"recurring\",\"bps\":2000,\"max_cycles\":12} | {\"type\":\"tiered\",\"bps\":1000,\"tiers\":[{\"min_conversions\":10,\"bps\":2000}]}. For multi-level programs add a \"multi_level\":[{\"level\":1,\"bps\":500}] ladder and set multi_level_enabled.", {
|
|
7303
|
+
owner_organization_id: z.string().describe("Org that owns the program"),
|
|
7304
|
+
name: z.string().describe("Program name"),
|
|
7305
|
+
commission: z.record(z.any()).describe("Commission model (tagged-union JSON)"),
|
|
7306
|
+
conversion_kind: z.enum([
|
|
7307
|
+
"event",
|
|
7308
|
+
"payment",
|
|
7309
|
+
"entitlement"
|
|
7310
|
+
]).optional().describe("Which qualifying signal counts as a conversion (default event)"),
|
|
7311
|
+
conversion_event_name: z.string().optional().describe("Tenant event name that counts as a conversion (for conversion_kind=event)"),
|
|
7312
|
+
conversion_entitlement_id: z.string().optional().describe("Entitlement id that counts (for conversion_kind=entitlement)"),
|
|
7313
|
+
attribution_model: z.enum(["first_touch", "last_touch"]).optional().describe("Which touch gets the credit (default last_touch)"),
|
|
7314
|
+
attribution_window_days: z.number().int().optional().describe("Days a touch remains attributable (0–365, default 30)"),
|
|
7315
|
+
allow_self_referral: z.boolean().optional().describe("Allow the owner org to enroll as its own affiliate (default false)"),
|
|
7316
|
+
multi_level_enabled: z.boolean().optional().describe("Pay upline overrides per the commission \"multi_level\" ladder (default false)"),
|
|
7317
|
+
currency: z.string().optional().describe("ISO currency (default usd)"),
|
|
7318
|
+
payout_schedule: z.enum([
|
|
7319
|
+
"manual",
|
|
7320
|
+
"weekly",
|
|
7321
|
+
"monthly"
|
|
7322
|
+
]).optional(),
|
|
7323
|
+
payout_threshold_cents: z.number().int().optional(),
|
|
7324
|
+
hold_period_days: z.number().int().optional().describe("Refund-window holdback before commissions become payable"),
|
|
7325
|
+
platform_fee_bps: z.number().int().optional().describe("Amba's cut on payouts (basis points)"),
|
|
7326
|
+
approval_mode: z.enum(["auto", "manual"]).optional(),
|
|
7327
|
+
funding_source: z.enum(["split_at_source", "external"]).optional().describe("Where commission money sources from (split_at_source default | external). Recorded today; the transfer mechanism is gated.")
|
|
7328
|
+
}, async (args, { client }) => {
|
|
7329
|
+
const result = await client.post("/affiliate/programs", args);
|
|
7330
|
+
return { content: [{
|
|
7331
|
+
type: "text",
|
|
7332
|
+
text: JSON.stringify(result, null, 2)
|
|
7333
|
+
}] };
|
|
7334
|
+
});
|
|
7335
|
+
registerTool(server, apiClient, "amba_affiliate_programs_list", "List affiliate programs owned by orgs you can access.", {}, async (_, { client }) => {
|
|
7336
|
+
const result = await client.get("/affiliate/programs");
|
|
7337
|
+
return { content: [{
|
|
7338
|
+
type: "text",
|
|
7339
|
+
text: JSON.stringify(result, null, 2)
|
|
7340
|
+
}] };
|
|
7341
|
+
});
|
|
7342
|
+
registerTool(server, apiClient, "amba_affiliate_programs_get", "Get an affiliate program by id.", { program_id: z.string() }, async ({ program_id }, { client }) => {
|
|
7343
|
+
const result = await client.get(`/affiliate/programs/${program_id}`);
|
|
7344
|
+
return { content: [{
|
|
7345
|
+
type: "text",
|
|
7346
|
+
text: JSON.stringify(result, null, 2)
|
|
7347
|
+
}] };
|
|
7348
|
+
});
|
|
7349
|
+
registerTool(server, apiClient, "amba_affiliate_programs_update", "Update an affiliate program (status, payout config, fee, approval mode, conversion definition, attribution, multi-level). Set status=\"active\" to launch; set multi_level_enabled=true to start paying upline overrides.", {
|
|
7350
|
+
program_id: z.string(),
|
|
7351
|
+
status: z.enum([
|
|
7352
|
+
"draft",
|
|
7353
|
+
"active",
|
|
7354
|
+
"paused",
|
|
7355
|
+
"archived"
|
|
7356
|
+
]).optional(),
|
|
7357
|
+
payout_threshold_cents: z.number().int().optional(),
|
|
7358
|
+
hold_period_days: z.number().int().optional(),
|
|
7359
|
+
platform_fee_bps: z.number().int().optional(),
|
|
7360
|
+
approval_mode: z.enum(["auto", "manual"]).optional(),
|
|
7361
|
+
funding_source: z.enum(["split_at_source", "external"]).optional(),
|
|
7362
|
+
payout_schedule: z.enum([
|
|
7363
|
+
"manual",
|
|
7364
|
+
"weekly",
|
|
7365
|
+
"monthly"
|
|
7366
|
+
]).optional(),
|
|
7367
|
+
name: z.string().optional(),
|
|
7368
|
+
conversion_kind: z.enum([
|
|
7369
|
+
"event",
|
|
7370
|
+
"payment",
|
|
7371
|
+
"entitlement"
|
|
7372
|
+
]).optional(),
|
|
7373
|
+
conversion_event_name: z.string().nullable().optional(),
|
|
7374
|
+
conversion_entitlement_id: z.string().nullable().optional(),
|
|
7375
|
+
attribution_model: z.enum(["first_touch", "last_touch"]).optional(),
|
|
7376
|
+
attribution_window_days: z.number().int().optional(),
|
|
7377
|
+
allow_self_referral: z.boolean().optional(),
|
|
7378
|
+
multi_level_enabled: z.boolean().optional()
|
|
7379
|
+
}, async ({ program_id, ...rest }, { client }) => {
|
|
7380
|
+
const result = await client.patch(`/affiliate/programs/${program_id}`, rest);
|
|
7381
|
+
return { content: [{
|
|
7382
|
+
type: "text",
|
|
7383
|
+
text: JSON.stringify(result, null, 2)
|
|
7384
|
+
}] };
|
|
7385
|
+
});
|
|
7386
|
+
registerTool(server, apiClient, "amba_affiliate_invite", "Enroll an affiliate. Pass affiliate_org_id to enroll an existing Amba org directly, or email to get guidance for the recipient to amba_affiliate_signup first. For multi-level programs, pass parent_enrollment_id to place the new affiliate under a sponsor (their upline).", {
|
|
7387
|
+
program_id: z.string(),
|
|
7388
|
+
affiliate_org_id: z.string().optional().describe("Existing affiliate org UUID to enroll"),
|
|
7389
|
+
email: z.string().optional().describe("Email of an affiliate who has not yet joined Amba"),
|
|
7390
|
+
parent_enrollment_id: z.string().optional().describe("Sponsor/upline enrollment id in this program (multi-level placement)")
|
|
7391
|
+
}, async ({ program_id, affiliate_org_id, email, parent_enrollment_id }, { client }) => {
|
|
7392
|
+
const body = {};
|
|
7393
|
+
if (affiliate_org_id !== void 0) body["affiliate_org_id"] = affiliate_org_id;
|
|
7394
|
+
if (email !== void 0) body["email"] = email;
|
|
7395
|
+
if (parent_enrollment_id !== void 0) body["parent_enrollment_id"] = parent_enrollment_id;
|
|
7396
|
+
const result = await client.post(`/affiliate/programs/${program_id}/invite`, body);
|
|
7397
|
+
return { content: [{
|
|
7398
|
+
type: "text",
|
|
7399
|
+
text: JSON.stringify(result, null, 2)
|
|
7400
|
+
}] };
|
|
7401
|
+
});
|
|
7402
|
+
registerTool(server, apiClient, "amba_affiliate_affiliates_list", "List a program’s affiliates with their conversion + commission totals.", { program_id: z.string() }, async ({ program_id }, { client }) => {
|
|
7403
|
+
const result = await client.get(`/affiliate/programs/${program_id}/affiliates`);
|
|
7404
|
+
return { content: [{
|
|
7405
|
+
type: "text",
|
|
7406
|
+
text: JSON.stringify(result, null, 2)
|
|
7407
|
+
}] };
|
|
7408
|
+
});
|
|
7409
|
+
registerTool(server, apiClient, "amba_affiliate_affiliate_approve", "Approve (activate) a pending affiliate enrollment.", {
|
|
7410
|
+
program_id: z.string(),
|
|
7411
|
+
enrollment_id: z.string()
|
|
7412
|
+
}, async ({ program_id, enrollment_id }, { client }) => {
|
|
7413
|
+
const result = await client.post(`/affiliate/programs/${program_id}/affiliates/${enrollment_id}/approve`, {});
|
|
7414
|
+
return { content: [{
|
|
7415
|
+
type: "text",
|
|
7416
|
+
text: JSON.stringify(result, null, 2)
|
|
7417
|
+
}] };
|
|
7418
|
+
});
|
|
7419
|
+
registerTool(server, apiClient, "amba_affiliate_affiliate_ban", "Ban an affiliate from a program (links die; pending conversions voided).", {
|
|
7420
|
+
program_id: z.string(),
|
|
7421
|
+
enrollment_id: z.string()
|
|
7422
|
+
}, async ({ program_id, enrollment_id }, { client }) => {
|
|
7423
|
+
const result = await client.post(`/affiliate/programs/${program_id}/affiliates/${enrollment_id}/ban`, {});
|
|
7424
|
+
return { content: [{
|
|
7425
|
+
type: "text",
|
|
7426
|
+
text: JSON.stringify(result, null, 2)
|
|
7427
|
+
}] };
|
|
7428
|
+
});
|
|
7429
|
+
registerTool(server, apiClient, "amba_affiliate_affiliate_pause", "Pause an affiliate enrollment (their links stop earning; reversible via approve).", {
|
|
7430
|
+
program_id: z.string(),
|
|
7431
|
+
enrollment_id: z.string()
|
|
7432
|
+
}, async ({ program_id, enrollment_id }, { client }) => {
|
|
7433
|
+
const result = await client.post(`/affiliate/programs/${program_id}/affiliates/${enrollment_id}/pause`, {});
|
|
7434
|
+
return { content: [{
|
|
7435
|
+
type: "text",
|
|
7436
|
+
text: JSON.stringify(result, null, 2)
|
|
7437
|
+
}] };
|
|
7438
|
+
});
|
|
7439
|
+
registerTool(server, apiClient, "amba_affiliate_affiliates_get", "Get one affiliate enrollment with its codes, conversions, and commission totals.", {
|
|
7440
|
+
program_id: z.string(),
|
|
7441
|
+
enrollment_id: z.string()
|
|
7442
|
+
}, async ({ program_id, enrollment_id }, { client }) => {
|
|
7443
|
+
const result = await client.get(`/affiliate/programs/${program_id}/affiliates/${enrollment_id}`);
|
|
7444
|
+
return { content: [{
|
|
7445
|
+
type: "text",
|
|
7446
|
+
text: JSON.stringify(result, null, 2)
|
|
7447
|
+
}] };
|
|
7448
|
+
});
|
|
7449
|
+
registerTool(server, apiClient, "amba_affiliate_conversions_list", "List conversions for a program (optionally filter by status).", {
|
|
7450
|
+
program_id: z.string(),
|
|
7451
|
+
status: z.enum([
|
|
7452
|
+
"pending",
|
|
7453
|
+
"approved",
|
|
7454
|
+
"rejected",
|
|
7455
|
+
"clawed_back"
|
|
7456
|
+
]).optional()
|
|
7457
|
+
}, async ({ program_id, status }, { client }) => {
|
|
7458
|
+
const qs = status ? `?status=${status}` : "";
|
|
7459
|
+
const result = await client.get(`/affiliate/programs/${program_id}/conversions${qs}`);
|
|
7460
|
+
return { content: [{
|
|
7461
|
+
type: "text",
|
|
7462
|
+
text: JSON.stringify(result, null, 2)
|
|
7463
|
+
}] };
|
|
7464
|
+
});
|
|
7465
|
+
registerTool(server, apiClient, "amba_affiliate_conversion_approve", "Approve a pending conversion so it accrues commission.", {
|
|
7466
|
+
program_id: z.string(),
|
|
7467
|
+
conversion_id: z.string()
|
|
7468
|
+
}, async ({ program_id, conversion_id }, { client }) => {
|
|
7469
|
+
const result = await client.post(`/affiliate/programs/${program_id}/conversions/${conversion_id}/approve`, {});
|
|
7470
|
+
return { content: [{
|
|
7471
|
+
type: "text",
|
|
7472
|
+
text: JSON.stringify(result, null, 2)
|
|
7473
|
+
}] };
|
|
7474
|
+
});
|
|
7475
|
+
registerTool(server, apiClient, "amba_affiliate_conversion_reject", "Reject a pending conversion so it never accrues commission.", {
|
|
7476
|
+
program_id: z.string(),
|
|
7477
|
+
conversion_id: z.string()
|
|
7478
|
+
}, async ({ program_id, conversion_id }, { client }) => {
|
|
7479
|
+
const result = await client.post(`/affiliate/programs/${program_id}/conversions/${conversion_id}/reject`, {});
|
|
7480
|
+
return { content: [{
|
|
7481
|
+
type: "text",
|
|
7482
|
+
text: JSON.stringify(result, null, 2)
|
|
7483
|
+
}] };
|
|
7484
|
+
});
|
|
7485
|
+
registerTool(server, apiClient, "amba_affiliate_conversion_clawback", "Claw back an approved/paid conversion (the underlying sale refunded or charged back). Pre-payout commissions are reversed; already-paid commissions become negative carry-forward netted against the affiliate’s next payout.", {
|
|
7486
|
+
program_id: z.string(),
|
|
7487
|
+
conversion_id: z.string(),
|
|
7488
|
+
reason: z.string().optional().describe("Why (e.g. refund, chargeback). Default \"refund\".")
|
|
7489
|
+
}, async ({ program_id, conversion_id, reason }, { client }) => {
|
|
7490
|
+
const body = {};
|
|
7491
|
+
if (reason !== void 0) body["reason"] = reason;
|
|
7492
|
+
const result = await client.post(`/affiliate/programs/${program_id}/conversions/${conversion_id}/clawback`, body);
|
|
7493
|
+
return { content: [{
|
|
7494
|
+
type: "text",
|
|
7495
|
+
text: JSON.stringify(result, null, 2)
|
|
7496
|
+
}] };
|
|
7497
|
+
});
|
|
7498
|
+
registerTool(server, apiClient, "amba_affiliate_analytics_get", "Program-level analytics: conversion + commission totals and an affiliate leaderboard.", { program_id: z.string() }, async ({ program_id }, { client }) => {
|
|
7499
|
+
const result = await client.get(`/affiliate/programs/${program_id}/analytics`);
|
|
7500
|
+
return { content: [{
|
|
7501
|
+
type: "text",
|
|
7502
|
+
text: JSON.stringify(result, null, 2)
|
|
7503
|
+
}] };
|
|
7504
|
+
});
|
|
7505
|
+
registerTool(server, apiClient, "amba_affiliate_leaderboard_get", "Affiliate leaderboard for a program: top affiliates by commission, with clicks + EPC.", {
|
|
7506
|
+
program_id: z.string(),
|
|
7507
|
+
limit: z.number().int().optional()
|
|
7508
|
+
}, async ({ program_id, limit }, { client }) => {
|
|
7509
|
+
const qs = limit !== void 0 ? `?limit=${limit}` : "";
|
|
7510
|
+
const result = await client.get(`/affiliate/programs/${program_id}/leaderboard${qs}`);
|
|
7511
|
+
return { content: [{
|
|
7512
|
+
type: "text",
|
|
7513
|
+
text: JSON.stringify(result, null, 2)
|
|
7514
|
+
}] };
|
|
7515
|
+
});
|
|
7516
|
+
registerTool(server, apiClient, "amba_affiliate_payouts_list", "Settlement (payout) history for a program’s affiliates: net amount, period, transfer id, status.", { program_id: z.string() }, async ({ program_id }, { client }) => {
|
|
7517
|
+
const result = await client.get(`/affiliate/programs/${program_id}/payouts`);
|
|
7518
|
+
return { content: [{
|
|
7519
|
+
type: "text",
|
|
7520
|
+
text: JSON.stringify(result, null, 2)
|
|
7521
|
+
}] };
|
|
7522
|
+
});
|
|
7523
|
+
registerTool(server, apiClient, "amba_affiliate_payout_run", "Run payouts for a program: pay each affiliate org their payable commissions over the threshold via Stripe Connect. Gated by ORG_PAYOUTS_LIVE (money movement).", { program_id: z.string() }, async ({ program_id }, { client }) => {
|
|
7524
|
+
const result = await client.post(`/affiliate/programs/${program_id}/payouts`, {});
|
|
7525
|
+
return { content: [{
|
|
7526
|
+
type: "text",
|
|
7527
|
+
text: JSON.stringify(result, null, 2)
|
|
7528
|
+
}] };
|
|
7529
|
+
});
|
|
7530
|
+
registerTool(server, apiClient, "amba_affiliate_my_programs_list", "List the affiliate programs your org(s) are enrolled in.", {}, async (_, { client }) => {
|
|
7531
|
+
const result = await client.get("/affiliate-me/programs");
|
|
7532
|
+
return { content: [{
|
|
7533
|
+
type: "text",
|
|
7534
|
+
text: JSON.stringify(result, null, 2)
|
|
7535
|
+
}] };
|
|
7536
|
+
});
|
|
7537
|
+
registerTool(server, apiClient, "amba_affiliate_my_invites_list", "List affiliate-program invitations pending for your org(s).", {}, async (_, { client }) => {
|
|
7538
|
+
const result = await client.get("/affiliate-me/invites");
|
|
7539
|
+
return { content: [{
|
|
7540
|
+
type: "text",
|
|
7541
|
+
text: JSON.stringify(result, null, 2)
|
|
7542
|
+
}] };
|
|
7543
|
+
});
|
|
7544
|
+
registerTool(server, apiClient, "amba_affiliate_my_invite_accept", "Accept an affiliate-program invitation addressed to your org (activates the enrollment and returns your referral code).", { enrollment_id: z.string() }, async ({ enrollment_id }, { client }) => {
|
|
7545
|
+
const result = await client.post(`/affiliate-me/invites/${enrollment_id}/accept`, {});
|
|
7546
|
+
return { content: [{
|
|
7547
|
+
type: "text",
|
|
7548
|
+
text: JSON.stringify(result, null, 2)
|
|
7549
|
+
}] };
|
|
7550
|
+
});
|
|
7551
|
+
registerTool(server, apiClient, "amba_affiliate_my_referral_generate", "Mint a FRESH referral code for a program you are enrolled in (rotates/replaces your current active code).", { program_id: z.string() }, async ({ program_id }, { client }) => {
|
|
7552
|
+
const result = await client.post(`/affiliate-me/programs/${program_id}/referral`, {});
|
|
7553
|
+
return { content: [{
|
|
7554
|
+
type: "text",
|
|
7555
|
+
text: JSON.stringify(result, null, 2)
|
|
7556
|
+
}] };
|
|
7557
|
+
});
|
|
7558
|
+
registerTool(server, apiClient, "amba_affiliate_my_conversions_list", "List conversions across your affiliate enrollments (optionally filter by status).", { status: z.enum([
|
|
7559
|
+
"pending",
|
|
7560
|
+
"approved",
|
|
7561
|
+
"rejected",
|
|
7562
|
+
"clawed_back"
|
|
7563
|
+
]).optional() }, async ({ status }, { client }) => {
|
|
7564
|
+
const qs = status ? `?status=${status}` : "";
|
|
7565
|
+
const result = await client.get(`/affiliate-me/conversions${qs}`);
|
|
7566
|
+
return { content: [{
|
|
7567
|
+
type: "text",
|
|
7568
|
+
text: JSON.stringify(result, null, 2)
|
|
7569
|
+
}] };
|
|
7570
|
+
});
|
|
7571
|
+
registerTool(server, apiClient, "amba_affiliate_my_payout_request", "Check what one of your orgs is owed and whether its payout account is ready. Does NOT move money — owners settle payable commissions on the program schedule; this surfaces the balance + readiness.", { org_id: z.string() }, async ({ org_id }, { client }) => {
|
|
7572
|
+
const result = await client.post("/affiliate-me/payout-request", { org_id });
|
|
7573
|
+
return { content: [{
|
|
7574
|
+
type: "text",
|
|
7575
|
+
text: JSON.stringify(result, null, 2)
|
|
7576
|
+
}] };
|
|
7577
|
+
});
|
|
7578
|
+
registerTool(server, apiClient, "amba_affiliate_my_referral_get", "Get your referral code(s) / link(s) for a program you are enrolled in.", { program_id: z.string() }, async ({ program_id }, { client }) => {
|
|
7579
|
+
const result = await client.get(`/affiliate-me/programs/${program_id}/referral`);
|
|
7580
|
+
return { content: [{
|
|
7581
|
+
type: "text",
|
|
7582
|
+
text: JSON.stringify(result, null, 2)
|
|
7583
|
+
}] };
|
|
7584
|
+
});
|
|
7585
|
+
registerTool(server, apiClient, "amba_affiliate_my_stats_get", "Your affiliate stats across all enrollments: conversions, earnings, pending vs paid.", {}, async (_, { client }) => {
|
|
7586
|
+
const result = await client.get("/affiliate-me/stats");
|
|
7587
|
+
return { content: [{
|
|
7588
|
+
type: "text",
|
|
7589
|
+
text: JSON.stringify(result, null, 2)
|
|
7590
|
+
}] };
|
|
7591
|
+
});
|
|
7592
|
+
registerTool(server, apiClient, "amba_affiliate_my_balance_get", "Your available (unpaid) and lifetime-paid commission balance, per currency.", {}, async (_, { client }) => {
|
|
7593
|
+
const result = await client.get("/affiliate-me/balance");
|
|
7594
|
+
return { content: [{
|
|
7595
|
+
type: "text",
|
|
7596
|
+
text: JSON.stringify(result, null, 2)
|
|
7597
|
+
}] };
|
|
7598
|
+
});
|
|
7599
|
+
registerTool(server, apiClient, "amba_affiliate_my_payouts_list", "Your payout/settlement history.", {}, async (_, { client }) => {
|
|
7600
|
+
const result = await client.get("/affiliate-me/payouts");
|
|
7601
|
+
return { content: [{
|
|
7602
|
+
type: "text",
|
|
7603
|
+
text: JSON.stringify(result, null, 2)
|
|
7604
|
+
}] };
|
|
7605
|
+
});
|
|
7606
|
+
registerTool(server, apiClient, "amba_affiliate_my_payout_create", "Create a Stripe Connect payout account for an org you own (so you can receive commissions).", {
|
|
7607
|
+
org_id: z.string(),
|
|
7608
|
+
country: z.string().optional().describe("2-letter country (default US)")
|
|
7609
|
+
}, async ({ org_id, country }, { client }) => {
|
|
7610
|
+
const body = { org_id };
|
|
7611
|
+
if (country !== void 0) body["country"] = country;
|
|
7612
|
+
const result = await client.post("/affiliate-me/payout-account", body);
|
|
7613
|
+
return { content: [{
|
|
7614
|
+
type: "text",
|
|
7615
|
+
text: JSON.stringify(result, null, 2)
|
|
7616
|
+
}] };
|
|
7617
|
+
});
|
|
7618
|
+
registerTool(server, apiClient, "amba_affiliate_my_payout_onboarding", "Get a hosted Stripe onboarding URL to finish setting up an org’s payout account. Surface the URL to the user as a copy box.", { org_id: z.string() }, async ({ org_id }, { client }) => {
|
|
7619
|
+
const result = await client.post("/affiliate-me/payout-account/link", { org_id });
|
|
7620
|
+
return { content: [{
|
|
7621
|
+
type: "text",
|
|
7622
|
+
text: JSON.stringify(result, null, 2)
|
|
7623
|
+
}] };
|
|
7624
|
+
});
|
|
7625
|
+
registerTool(server, apiClient, "amba_affiliate_my_payout_status", "Check whether an org’s payout account is set up.", { org_id: z.string() }, async ({ org_id }, { client }) => {
|
|
7626
|
+
const result = await client.get(`/affiliate-me/payout-account?org_id=${org_id}`);
|
|
7627
|
+
return { content: [{
|
|
7628
|
+
type: "text",
|
|
7629
|
+
text: JSON.stringify(result, null, 2)
|
|
7630
|
+
}] };
|
|
7631
|
+
});
|
|
7632
|
+
}
|
|
7633
|
+
//#endregion
|
|
6167
7634
|
//#region src/tools/webhooks.ts
|
|
6168
|
-
function registerTools$
|
|
7635
|
+
function registerTools$2(server, apiClient) {
|
|
6169
7636
|
registerTool(server, apiClient, "amba_webhooks_create", "Register an outbound webhook subscription. Amba sends an HMAC-signed POST to target_url every time an event named event_name fires. The signing secret is returned ONCE in this response — copy it into your receiver config; later reads never include it. target_url must be https:// (http:// is allowed only for localhost during development).", {
|
|
6170
7637
|
project_id: z.string().describe("The project ID"),
|
|
6171
7638
|
event_name: z.string().describe("Event name to subscribe to. Must match ^[a-z][a-z0-9_.-]{0,127}$ (case-insensitive)."),
|
|
@@ -6288,7 +7755,7 @@ function registerTools$1(server, apiClient) {
|
|
|
6288
7755
|
}
|
|
6289
7756
|
//#endregion
|
|
6290
7757
|
//#region src/tools/email.ts
|
|
6291
|
-
function registerTools(server, apiClient) {
|
|
7758
|
+
function registerTools$1(server, apiClient) {
|
|
6292
7759
|
registerTool(server, apiClient, "amba_email_templates_create", "Create or update a transactional email template (upsert keyed by name — re-creating an existing name overwrites it). Templates are referenced by name when sending. Subject and html_body are required; text_body is an optional plain-text fallback. name must match ^[a-z][a-z0-9_]*$.", {
|
|
6293
7760
|
project_id: z.string().describe("The project ID"),
|
|
6294
7761
|
name: z.string().describe("Template name. Must match ^[a-z][a-z0-9_]*$."),
|
|
@@ -6440,11 +7907,56 @@ function registerTools(server, apiClient) {
|
|
|
6440
7907
|
text: JSON.stringify(result, null, 2)
|
|
6441
7908
|
}] };
|
|
6442
7909
|
});
|
|
6443
|
-
registerTool(server, apiClient, "amba_email_deliveries_get", "Describe a single email delivery by id, including recipient, template, status, and last error if it failed.", {
|
|
7910
|
+
registerTool(server, apiClient, "amba_email_deliveries_get", "Describe a single email delivery by id, including recipient, template, status, and last error if it failed.", {
|
|
7911
|
+
project_id: z.string().describe("The project ID"),
|
|
7912
|
+
delivery_id: z.string().describe("The delivery UUID")
|
|
7913
|
+
}, async ({ project_id, delivery_id }, { client }) => {
|
|
7914
|
+
const result = await client.get(`/projects/${project_id}/email/deliveries/${delivery_id}`);
|
|
7915
|
+
return { content: [{
|
|
7916
|
+
type: "text",
|
|
7917
|
+
text: JSON.stringify(result, null, 2)
|
|
7918
|
+
}] };
|
|
7919
|
+
});
|
|
7920
|
+
}
|
|
7921
|
+
//#endregion
|
|
7922
|
+
//#region src/tools/app-mcp.ts
|
|
7923
|
+
function registerTools(server, apiClient) {
|
|
7924
|
+
registerTool(server, apiClient, "amba_app_mcp_get_config", "Read the configuration of this app's agent surface (the per-app MCP endpoint whose tools are the app's own collections and functions). Returns: enabled flag, hostname slug, auth_mode (which credential types may connect: all / client_only / developer_only), the exposure allowlist (which collections/functions are projected into agent tools — DEFAULT: none), and ready-to-use connection info (`connection.url` works today; `connection.hostname_url` appears once a slug is set, with a note on activation timing).", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
|
|
7925
|
+
const result = await client.get(`/projects/${project_id}/app-mcp/config`);
|
|
7926
|
+
return { content: [{
|
|
7927
|
+
type: "text",
|
|
7928
|
+
text: JSON.stringify(result, null, 2)
|
|
7929
|
+
}] };
|
|
7930
|
+
});
|
|
7931
|
+
registerTool(server, apiClient, "amba_app_mcp_update_config", "Update the app agent surface configuration. `enabled: false` turns the whole surface off (existing connections lose their tools on the next request). `slug` reserves a friendly hostname label for the surface (lowercase DNS label, globally unique — 409 SLUG_TAKEN on collision; null clears it). `auth_mode` restricts who may connect: \"all\" (default — developer tokens and app client keys), \"client_only\" (only the app's client keys + end-user sessions), or \"developer_only\". Provide at least one field.", {
|
|
6444
7932
|
project_id: z.string().describe("The project ID"),
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
7933
|
+
enabled: z.boolean().optional().describe("Turn the agent surface on/off"),
|
|
7934
|
+
slug: z.string().nullable().optional().describe("Hostname label (lowercase DNS label) or null to clear"),
|
|
7935
|
+
auth_mode: z.enum([
|
|
7936
|
+
"all",
|
|
7937
|
+
"client_only",
|
|
7938
|
+
"developer_only"
|
|
7939
|
+
]).optional().describe("Which credential types may connect")
|
|
7940
|
+
}, async ({ project_id, enabled, slug, auth_mode }, { client }) => {
|
|
7941
|
+
const body = {};
|
|
7942
|
+
if (enabled !== void 0) body.enabled = enabled;
|
|
7943
|
+
if (slug !== void 0) body.slug = slug;
|
|
7944
|
+
if (auth_mode !== void 0) body.auth_mode = auth_mode;
|
|
7945
|
+
const result = await client.patch(`/projects/${project_id}/app-mcp/config`, body);
|
|
7946
|
+
return { content: [{
|
|
7947
|
+
type: "text",
|
|
7948
|
+
text: JSON.stringify(result, null, 2)
|
|
7949
|
+
}] };
|
|
7950
|
+
});
|
|
7951
|
+
registerTool(server, apiClient, "amba_app_mcp_set_exposure", "Set which collections and deployed functions the app agent surface exposes as typed tools. EXPLICIT ALLOWLIST with full-set replace semantics per provided kind: `collections: [\"recipes\"]` makes recipes the ONLY exposed collection; an empty array hides everything of that kind; an omitted kind is left unchanged. Nothing is exposed by default — after creating a collection or deploying a function, call this to make it an agent tool. Functions additionally pick up the typed input schema they declared at deploy time (metadata `mcp.input_schema`).", {
|
|
7952
|
+
project_id: z.string().describe("The project ID"),
|
|
7953
|
+
collections: z.array(z.string()).optional().describe("Full replacement list of exposed collection names"),
|
|
7954
|
+
functions: z.array(z.string()).optional().describe("Full replacement list of exposed function names")
|
|
7955
|
+
}, async ({ project_id, collections, functions }, { client }) => {
|
|
7956
|
+
const body = {};
|
|
7957
|
+
if (collections !== void 0) body.collections = collections;
|
|
7958
|
+
if (functions !== void 0) body.functions = functions;
|
|
7959
|
+
const result = await client.put(`/projects/${project_id}/app-mcp/exposure`, body);
|
|
6448
7960
|
return { content: [{
|
|
6449
7961
|
type: "text",
|
|
6450
7962
|
text: JSON.stringify(result, null, 2)
|
|
@@ -7738,32 +9250,33 @@ const AMBA_SETUP_SOCIAL_MIME = "text/markdown";
|
|
|
7738
9250
|
* Companion to `AMBA_SETUP_GUIDE_MD`. Step 3 of the main playbook
|
|
7739
9251
|
* tells the agent to fetch this resource when wiring infrastructure.
|
|
7740
9252
|
*
|
|
7741
|
-
* Surface scope: collections (
|
|
9253
|
+
* Surface scope: collections (relational Postgres tables), functions
|
|
7742
9254
|
* (serverless), analytics, AI prompts, secrets, configs / feature
|
|
7743
9255
|
* flags, third-party integrations, media (file storage + CDN),
|
|
7744
9256
|
* sites (static asset hosting).
|
|
7745
9257
|
*
|
|
7746
9258
|
* Twin: `packages/cli/skill-bundle/references/infrastructure.md`.
|
|
7747
|
-
* Drift gate in `amba-setup.test.ts`.
|
|
7748
|
-
*
|
|
9259
|
+
* Drift gate in `amba-setup.test.ts`. Collections are named as relational
|
|
9260
|
+
* Postgres tables; the hosting supplier (Neon) and the rest of the infra
|
|
9261
|
+
* stack (Temporal / R2) stay scrubbed before exposing on the wire.
|
|
7749
9262
|
*/
|
|
7750
9263
|
const AMBA_SETUP_INFRASTRUCTURE_MD = `# Infrastructure
|
|
7751
9264
|
|
|
7752
|
-
The plumbing that sits behind every other surface:
|
|
9265
|
+
The plumbing that sits behind every other surface: relational Postgres tables (Collections — schema-first, per-tenant), serverless functions (run server-side code without standing up a backend), analytics (events + sessions), AI prompts (managed LLM templates, callable from the SDK with per-tenant keys), secrets, runtime configs, feature flags, third-party integrations (RevenueCat / Superwall / Stripe / push credentials), media (file storage + CDN), and sites (static asset hosting at \`*.app.amba.host\`).
|
|
7753
9266
|
|
|
7754
9267
|
If gamification, economy, and social are the playable surface, **infrastructure is what you build a custom product on top of**. Anything that doesn't fit the canned surfaces lands here.
|
|
7755
9268
|
|
|
7756
9269
|
## MCP tools
|
|
7757
9270
|
|
|
7758
|
-
### Collections (
|
|
9271
|
+
### Collections (relational Postgres tables)
|
|
7759
9272
|
|
|
7760
|
-
A collection is a
|
|
9273
|
+
A collection is a relational Postgres table inside the project's isolated tenant database — typed columns, foreign keys, transactions, unique indexes, and vector search. You describe the columns, the server creates the table and any indexes. Rows are scoped to the signed-in \`app_user\` automatically (server-enforced auto row-level isolation) for SDK clients — admin tools bypass this.
|
|
7761
9274
|
|
|
7762
9275
|
Admin tools authenticate the developer/agent (pass \`pat\` or send it as the inbound Bearer) and take \`project_id\`. Client tools authenticate an end-user and take \`api_key\` (+ \`session_token\`) — NOT \`project_id\` and NOT a \`pat\`. Every row tool names the collection with \`name\`, never \`collection\`.
|
|
7763
9276
|
|
|
7764
9277
|
| Tool | Purpose | Example args |
|
|
7765
9278
|
| --- | --- | --- |
|
|
7766
|
-
| \`amba_collections_create\` | Create a typed collection. Pass \`shared: true\` for developer-seeded GLOBAL content (question banks, lookup tables) so \`user_id\` is nullable. | \`{ project_id, name: "todos", columns: [{ name: "title", type: "text", nullable: false }, { name: "done", type: "boolean", nullable: false
|
|
9279
|
+
| \`amba_collections_create\` | Create a typed collection. Pass \`shared: true\` for developer-seeded GLOBAL content (question banks, lookup tables) so \`user_id\` is nullable. | \`{ project_id, name: "todos", columns: [{ name: "title", type: "text", nullable: false }, { name: "done", type: "boolean", nullable: false }, { name: "due_at", type: "timestamptz", nullable: true }], shared: false }\` |
|
|
7767
9280
|
| \`amba_collections_list\` | List collections in this project. | \`{ project_id }\` |
|
|
7768
9281
|
| \`amba_collections_get\` | Read one collection's schema. | \`{ project_id, name: "todos" }\` |
|
|
7769
9282
|
| \`amba_collections_alter\` | Exactly ONE of: \`add_column\`, \`add_index\`, \`drop_column\`, or \`relax_user_id\` per call. \`relax_user_id: true\` converts an existing collection to shared (drops the \`user_id\` NOT NULL). | \`{ project_id, name: "todos", add_column: { name: "priority", type: "integer", nullable: true } }\` |
|
|
@@ -7780,7 +9293,7 @@ Admin tools authenticate the developer/agent (pass \`pat\` or send it as the inb
|
|
|
7780
9293
|
| \`amba_client_find_rows\` | Filter / sort / paginate rows (SDK-shaped \`filter\`). | \`{ api_key, session_token, name: "todos", filter: {...}, order: ["created_at desc"], limit: 50 }\` |
|
|
7781
9294
|
| \`amba_client_find_nearest_rows\` | Vector-similarity search (rows with a \`vector(<dim>)\` column). | \`{ api_key, session_token, name: "todos", column: "embedding", to_vector: [...], k: 10 }\` |
|
|
7782
9295
|
|
|
7783
|
-
Column types: \`text\`, \`integer\`, \`bigint\`, \`numeric\`, \`boolean\`, \`timestamptz\`, \`date\`, \`jsonb\`, \`uuid\`, \`vector(
|
|
9296
|
+
Column types: \`text\`, \`integer\`, \`bigint\`, \`numeric\`, \`boolean\`, \`timestamptz\`, \`date\`, \`jsonb\`, \`uuid\`, \`vector\` (pass a separate \`dimension\` field, e.g. \`{ name: "embedding", type: "vector", dimension: 1536 }\` for OpenAI embeddings), plus array forms \`text[]\`, \`integer[]\`, \`bigint[]\`, \`numeric[]\`, \`boolean[]\`, \`uuid[]\`. Columns are NOT NULL unless \`nullable: true\`; column defaults are not supported (set values at insert time). Use \`integer\` (not \`int\`), \`numeric\` (not \`float\`/\`real\`/\`double\`), and \`jsonb\` (not \`json\`) — the validator rejects the aliases.
|
|
7784
9297
|
|
|
7785
9298
|
### Functions (serverless code)
|
|
7786
9299
|
|
|
@@ -7797,6 +9310,19 @@ Run user code in a sandbox triggered by HTTP, cron, or webhook. The function get
|
|
|
7797
9310
|
| \`amba_functions_pause_schedule\` | Pause a scheduled trigger without deleting it. | \`{ project_id, function_id }\` |
|
|
7798
9311
|
| \`amba_functions_resume_schedule\` | Resume. | \`{ project_id, function_id }\` |
|
|
7799
9312
|
| \`amba_functions_trigger_schedule\` | Fire a scheduled function ad-hoc (testing). | \`{ project_id, function_id }\` |
|
|
9313
|
+
| \`amba_function_domains_attach\` | Attach one exact hostname to one function; returns DNS validation instructions. | \`{ project_id, name: "feed", hostname: "feeds.example.com" }\` |
|
|
9314
|
+
| \`amba_function_domains_list\` | List provider-neutral hostname, ownership, and certificate status. | \`{ project_id, name: "feed" }\` |
|
|
9315
|
+
| \`amba_function_domains_refresh\` | Re-poll DNS ownership and certificate state. | \`{ project_id, name: "feed", hostname: "feeds.example.com" }\` |
|
|
9316
|
+
| \`amba_function_domains_remove\` | Detach an exact function hostname. | \`{ project_id, name: "feed", hostname: "feeds.example.com" }\` |
|
|
9317
|
+
|
|
9318
|
+
Function-domain routing preserves the complete incoming path and query string.
|
|
9319
|
+
It is exact-host only (no wildcard/path rewrite and no automatic \`www\` for a
|
|
9320
|
+
subdomain). Podcast/feed clients cannot attach an Amba API key, so their
|
|
9321
|
+
function must be deployed with \`public: true\` and validate any private token
|
|
9322
|
+
inside the handler. Effective-tier caps are free 1, pro 5, scale 20, and
|
|
9323
|
+
enterprise/comped 50; attach is limited to five attempts per project per hour.
|
|
9324
|
+
Unverified claims become eligible for reclaim after 24 hours, and routing
|
|
9325
|
+
resources are allocated only after ownership is active.
|
|
7800
9326
|
|
|
7801
9327
|
### AI prompts
|
|
7802
9328
|
|
|
@@ -7815,7 +9341,7 @@ Managed LLM templates: a stored prompt with provider + model + system message, i
|
|
|
7815
9341
|
| \`amba_ai_prompts_list\` | List prompts. | \`{ project_id }\` |
|
|
7816
9342
|
| \`amba_ai_prompts_get\` | Read one prompt. | \`{ project_id, name }\` |
|
|
7817
9343
|
| \`amba_ai_prompts_update\` | Edit a prompt (replaces all fields; bumps version). | \`{ project_id, name, provider, model, system_prompt: "..." }\` |
|
|
7818
|
-
| \`amba_ai_prompts_invoke\` | Invoke
|
|
9344
|
+
| \`amba_ai_prompts_invoke\` | Invoke by name server-side (admin testing; works with \`client_invokable: false\`). Uses the named gateway path, so the prompt budget, rate limit, token cap, and spend attribution are enforced. | \`{ project_id, name, messages: [{ role: "user", content: "..." }] }\` |
|
|
7819
9345
|
| \`amba_ai_prompts_delete\` | Delete. | \`{ project_id, name }\` |
|
|
7820
9346
|
|
|
7821
9347
|
### Analytics + events + sessions
|
|
@@ -7832,11 +9358,15 @@ Managed LLM templates: a stored prompt with provider + model + system message, i
|
|
|
7832
9358
|
|
|
7833
9359
|
### Secrets + configs + integrations
|
|
7834
9360
|
|
|
7835
|
-
Secrets here
|
|
9361
|
+
Secrets here become environment bindings on deployed functions. Omit \`function\`
|
|
9362
|
+
for a project-wide secret or pass it to scope the value to one function. Setting
|
|
9363
|
+
or rotating a secret queues an asynchronous update for already-deployed
|
|
9364
|
+
functions; later deployments reconcile the binding too. They are NOT where AI
|
|
9365
|
+
provider keys go (use \`amba_ai_providers_set\` for those — see AI prompts above).
|
|
7836
9366
|
|
|
7837
9367
|
| Tool | Purpose | Example args |
|
|
7838
9368
|
| --- | --- | --- |
|
|
7839
|
-
| \`amba_secrets_set\` | Set a function
|
|
9369
|
+
| \`amba_secrets_set\` | Set or rotate a function secret; omit \`function\` for project-wide scope or pass it for one function. Already-deployed functions receive it asynchronously. | \`{ project_id, name: "STRIPE_WEBHOOK_SECRET", value: "whsec_..." }\` |
|
|
7840
9370
|
| \`amba_secrets_get\` | Read a secret (returns \`"<redacted>"\` unless explicitly requested). | \`{ project_id, name }\` |
|
|
7841
9371
|
| \`amba_secrets_list\` | List secret names. | \`{ project_id }\` |
|
|
7842
9372
|
| \`amba_secrets_delete\` | Delete. | \`{ project_id, name }\` |
|
|
@@ -7873,6 +9403,23 @@ Secrets here are **function-scoped** — they become environment bindings on you
|
|
|
7873
9403
|
| \`amba_sites_remove_domain\` | Detach a domain. | \`{ project_id, site_id, domain }\` |
|
|
7874
9404
|
| \`amba_sites_delete\` | Delete a site. | \`{ project_id, site_id }\` |
|
|
7875
9405
|
|
|
9406
|
+
### Purchased domains + email forwarding
|
|
9407
|
+
|
|
9408
|
+
| Tool | Purpose | Example args |
|
|
9409
|
+
| --- | --- | --- |
|
|
9410
|
+
| \`amba_domains_search\` | Search available domains (free). | \`{ project_id, query: "myapp" }\` |
|
|
9411
|
+
| \`amba_domains_check\` | Check authoritative price + availability. | \`{ project_id, domains: ["myapp.com"] }\` |
|
|
9412
|
+
| \`amba_domains_purchase\` | Quote, then confirm, a domain purchase. | \`{ project_id, domain: "myapp.com", site: "marketing" }\` |
|
|
9413
|
+
| \`amba_domains_list\` | List purchased domains. | \`{ project_id }\` |
|
|
9414
|
+
| \`amba_domains_email_enable\` | Enable inbound routing when no MX conflict exists. | \`{ project_id, domain: "myapp.com" }\` |
|
|
9415
|
+
| \`amba_domains_email_destinations_add\` | Add a destination mailbox; returns action-required until verified. | \`{ project_id, domain: "myapp.com", email: "owner@example.net" }\` |
|
|
9416
|
+
| \`amba_domains_email_destinations_get\` | Poll destination verification. | \`{ project_id, domain: "myapp.com", destination_id }\` |
|
|
9417
|
+
| \`amba_domains_email_forwards_set\` | Create/update a literal forward. | \`{ project_id, domain: "myapp.com", source: "support", destination: "owner@example.net" }\` |
|
|
9418
|
+
| \`amba_domains_email_forwards_list\` | List literal forwards. | \`{ project_id, domain: "myapp.com" }\` |
|
|
9419
|
+
| \`amba_domains_email_forwards_delete\` | Delete a literal forward. | \`{ project_id, domain: "myapp.com", forward_id }\` |
|
|
9420
|
+
| \`amba_domains_email_catch_all_set\` | Enable/update/disable catch-all. | \`{ project_id, domain: "myapp.com", enabled: true, destination: "owner@example.net" }\` |
|
|
9421
|
+
| \`amba_domains_email_catch_all_get\` | Read catch-all state. | \`{ project_id, domain: "myapp.com" }\` |
|
|
9422
|
+
|
|
7876
9423
|
## SDK init per stack
|
|
7877
9424
|
|
|
7878
9425
|
\`Amba.configure(...)\` runs first. The infrastructure surfaces — collections, AI, config, flags, events — are SDK-side reads; the snippets below show what the client calls look like.
|
|
@@ -8013,15 +9560,13 @@ Batch.
|
|
|
8013
9560
|
|
|
8014
9561
|
4. **Analytics:** which tracker do you want?
|
|
8015
9562
|
- Only Amba's built-in events (recommended — already wired)
|
|
8016
|
-
- Amba +
|
|
9563
|
+
- Amba + your own analytics pipeline (subscribe a webhook to project events via \`amba_webhooks_create\` and forward server-side)
|
|
8017
9564
|
- None (rarely useful — events drive XP / achievements / streaks; disabling cripples gamification)
|
|
8018
9565
|
|
|
8019
9566
|
5. **Third-party integrations to set up:**
|
|
8020
9567
|
- [ ] RevenueCat (IAP / subscriptions on iOS + Android)
|
|
8021
9568
|
- [ ] Superwall (paywall A/B)
|
|
8022
|
-
- [ ]
|
|
8023
|
-
- [ ] Stripe (web payments / subscriptions)
|
|
8024
|
-
- [ ] Mixpanel / PostHog / Segment (analytics forwarding)
|
|
9569
|
+
- [ ] Stripe Billing (web subscriptions through the app's own Stripe account — provider \`stripe_billing\`)
|
|
8025
9570
|
- [ ] OpenAI / Anthropic / Mistral / Gemini LLM keys (required for \`Amba.ai.*\` — set via \`amba_ai_providers_set\`, **not** \`amba_integrations_configure\`)
|
|
8026
9571
|
|
|
8027
9572
|
6. **Feature flags:** seed any starter flags?
|
|
@@ -8279,6 +9824,9 @@ const TOOL_CATEGORY = {
|
|
|
8279
9824
|
amba_content_list_items: "engagement",
|
|
8280
9825
|
amba_content_update_item: "engagement",
|
|
8281
9826
|
amba_content_delete_item: "engagement",
|
|
9827
|
+
amba_content_translations_set: "engagement",
|
|
9828
|
+
amba_content_translations_list: "engagement",
|
|
9829
|
+
amba_content_translations_delete: "engagement",
|
|
8282
9830
|
amba_content_bulk_import: "engagement",
|
|
8283
9831
|
amba_content_schedules_create: "engagement",
|
|
8284
9832
|
amba_create_content_schedule: "engagement",
|
|
@@ -8361,6 +9909,17 @@ const TOOL_CATEGORY = {
|
|
|
8361
9909
|
amba_delete_challenge: "gamification",
|
|
8362
9910
|
amba_challenges_list_participants: "gamification",
|
|
8363
9911
|
amba_list_challenge_participants: "gamification",
|
|
9912
|
+
amba_monetization_plan: "economy",
|
|
9913
|
+
amba_monetization_drift: "economy",
|
|
9914
|
+
amba_monetization_export: "economy",
|
|
9915
|
+
amba_monetization_definitions_list: "economy",
|
|
9916
|
+
amba_monetization_adopt: "economy",
|
|
9917
|
+
amba_monetization_apply: "economy",
|
|
9918
|
+
amba_entitlements_define: "economy",
|
|
9919
|
+
amba_products_create: "economy",
|
|
9920
|
+
amba_entitlements_map_product: "economy",
|
|
9921
|
+
amba_offerings_create: "economy",
|
|
9922
|
+
amba_offerings_list: "economy",
|
|
8364
9923
|
amba_currencies_create: "economy",
|
|
8365
9924
|
amba_create_currency: "economy",
|
|
8366
9925
|
amba_currencies_list: "economy",
|
|
@@ -8470,6 +10029,7 @@ const TOOL_CATEGORY = {
|
|
|
8470
10029
|
amba_events_list: "analytics",
|
|
8471
10030
|
amba_events_count: "analytics",
|
|
8472
10031
|
amba_events_track: "analytics",
|
|
10032
|
+
amba_events_explain: "analytics",
|
|
8473
10033
|
amba_analytics_get: "analytics",
|
|
8474
10034
|
amba_get_analytics: "analytics",
|
|
8475
10035
|
amba_sessions_list: "analytics",
|
|
@@ -8502,6 +10062,48 @@ const TOOL_CATEGORY = {
|
|
|
8502
10062
|
amba_reprovision_project: "infrastructure",
|
|
8503
10063
|
amba_projects_get_provisioning_status: "infrastructure",
|
|
8504
10064
|
amba_get_provisioning_status: "infrastructure",
|
|
10065
|
+
amba_orgs_create: "infrastructure",
|
|
10066
|
+
amba_orgs_list: "infrastructure",
|
|
10067
|
+
amba_orgs_get: "infrastructure",
|
|
10068
|
+
amba_orgs_update: "infrastructure",
|
|
10069
|
+
amba_orgs_invite_member: "infrastructure",
|
|
10070
|
+
amba_orgs_transfer_owner: "infrastructure",
|
|
10071
|
+
amba_provision_app: "infrastructure",
|
|
10072
|
+
amba_service_accounts_create: "infrastructure",
|
|
10073
|
+
amba_service_accounts_list: "infrastructure",
|
|
10074
|
+
amba_service_accounts_revoke: "infrastructure",
|
|
10075
|
+
amba_affiliate_signup: "economy",
|
|
10076
|
+
amba_affiliate_programs_create: "economy",
|
|
10077
|
+
amba_affiliate_programs_list: "economy",
|
|
10078
|
+
amba_affiliate_programs_get: "economy",
|
|
10079
|
+
amba_affiliate_programs_update: "economy",
|
|
10080
|
+
amba_affiliate_invite: "economy",
|
|
10081
|
+
amba_affiliate_affiliates_list: "economy",
|
|
10082
|
+
amba_affiliate_affiliates_get: "economy",
|
|
10083
|
+
amba_affiliate_affiliate_approve: "economy",
|
|
10084
|
+
amba_affiliate_affiliate_pause: "economy",
|
|
10085
|
+
amba_affiliate_affiliate_ban: "economy",
|
|
10086
|
+
amba_affiliate_conversions_list: "economy",
|
|
10087
|
+
amba_affiliate_conversion_approve: "economy",
|
|
10088
|
+
amba_affiliate_conversion_reject: "economy",
|
|
10089
|
+
amba_affiliate_conversion_clawback: "economy",
|
|
10090
|
+
amba_affiliate_analytics_get: "economy",
|
|
10091
|
+
amba_affiliate_leaderboard_get: "economy",
|
|
10092
|
+
amba_affiliate_payouts_list: "economy",
|
|
10093
|
+
amba_affiliate_payout_run: "economy",
|
|
10094
|
+
amba_affiliate_my_programs_list: "economy",
|
|
10095
|
+
amba_affiliate_my_referral_get: "economy",
|
|
10096
|
+
amba_affiliate_my_referral_generate: "economy",
|
|
10097
|
+
amba_affiliate_my_stats_get: "economy",
|
|
10098
|
+
amba_affiliate_my_balance_get: "economy",
|
|
10099
|
+
amba_affiliate_my_invites_list: "economy",
|
|
10100
|
+
amba_affiliate_my_invite_accept: "economy",
|
|
10101
|
+
amba_affiliate_my_conversions_list: "economy",
|
|
10102
|
+
amba_affiliate_my_payouts_list: "economy",
|
|
10103
|
+
amba_affiliate_my_payout_request: "economy",
|
|
10104
|
+
amba_affiliate_my_payout_create: "economy",
|
|
10105
|
+
amba_affiliate_my_payout_onboarding: "economy",
|
|
10106
|
+
amba_affiliate_my_payout_status: "economy",
|
|
8505
10107
|
amba_collections_create: "infrastructure",
|
|
8506
10108
|
amba_create_collection: "infrastructure",
|
|
8507
10109
|
amba_collections_list: "infrastructure",
|
|
@@ -8547,6 +10149,10 @@ const TOOL_CATEGORY = {
|
|
|
8547
10149
|
amba_resume_function_schedule: "infrastructure",
|
|
8548
10150
|
amba_functions_trigger_schedule: "infrastructure",
|
|
8549
10151
|
amba_trigger_function_schedule: "infrastructure",
|
|
10152
|
+
amba_function_domains_attach: "infrastructure",
|
|
10153
|
+
amba_function_domains_list: "infrastructure",
|
|
10154
|
+
amba_function_domains_refresh: "infrastructure",
|
|
10155
|
+
amba_function_domains_remove: "infrastructure",
|
|
8550
10156
|
amba_sites_list: "infrastructure",
|
|
8551
10157
|
amba_list_sites: "infrastructure",
|
|
8552
10158
|
amba_sites_get: "infrastructure",
|
|
@@ -8557,6 +10163,8 @@ const TOOL_CATEGORY = {
|
|
|
8557
10163
|
amba_delete_site: "infrastructure",
|
|
8558
10164
|
amba_sites_add_domain: "infrastructure",
|
|
8559
10165
|
amba_add_site_domain: "infrastructure",
|
|
10166
|
+
amba_sites_refresh_domain: "infrastructure",
|
|
10167
|
+
amba_refresh_site_domain: "infrastructure",
|
|
8560
10168
|
amba_sites_remove_domain: "infrastructure",
|
|
8561
10169
|
amba_remove_site_domain: "infrastructure",
|
|
8562
10170
|
amba_sites_list_domains: "infrastructure",
|
|
@@ -8565,6 +10173,16 @@ const TOOL_CATEGORY = {
|
|
|
8565
10173
|
amba_domains_check: "infrastructure",
|
|
8566
10174
|
amba_domains_purchase: "infrastructure",
|
|
8567
10175
|
amba_domains_list: "infrastructure",
|
|
10176
|
+
amba_domains_email_status: "infrastructure",
|
|
10177
|
+
amba_domains_email_enable: "infrastructure",
|
|
10178
|
+
amba_domains_email_destinations_add: "infrastructure",
|
|
10179
|
+
amba_domains_email_destinations_list: "infrastructure",
|
|
10180
|
+
amba_domains_email_destinations_get: "infrastructure",
|
|
10181
|
+
amba_domains_email_forwards_set: "infrastructure",
|
|
10182
|
+
amba_domains_email_forwards_list: "infrastructure",
|
|
10183
|
+
amba_domains_email_forwards_delete: "infrastructure",
|
|
10184
|
+
amba_domains_email_catch_all_get: "infrastructure",
|
|
10185
|
+
amba_domains_email_catch_all_set: "infrastructure",
|
|
8568
10186
|
amba_operations_get: "infrastructure",
|
|
8569
10187
|
amba_operations_list: "infrastructure",
|
|
8570
10188
|
amba_media_upload: "infrastructure",
|
|
@@ -8575,6 +10193,13 @@ const TOOL_CATEGORY = {
|
|
|
8575
10193
|
amba_media_create_folder: "infrastructure",
|
|
8576
10194
|
amba_media_delete_folder: "infrastructure",
|
|
8577
10195
|
amba_media_list_folders: "infrastructure",
|
|
10196
|
+
amba_media_catalogs_create: "infrastructure",
|
|
10197
|
+
amba_media_catalogs_list: "infrastructure",
|
|
10198
|
+
amba_media_catalogs_get: "infrastructure",
|
|
10199
|
+
amba_media_catalogs_update: "infrastructure",
|
|
10200
|
+
amba_media_catalogs_delete: "infrastructure",
|
|
10201
|
+
amba_media_catalogs_add_item: "infrastructure",
|
|
10202
|
+
amba_media_catalogs_remove_item: "infrastructure",
|
|
8578
10203
|
amba_secrets_set: "infrastructure",
|
|
8579
10204
|
amba_set_secret: "infrastructure",
|
|
8580
10205
|
amba_secrets_get: "infrastructure",
|
|
@@ -8613,11 +10238,23 @@ const TOOL_CATEGORY = {
|
|
|
8613
10238
|
amba_delete_ai_prompt: "infrastructure",
|
|
8614
10239
|
amba_ai_prompts_invoke: "infrastructure",
|
|
8615
10240
|
amba_invoke_ai_prompt: "infrastructure",
|
|
10241
|
+
amba_ai_prompts_set_budget: "infrastructure",
|
|
10242
|
+
amba_ai_prompts_get_spend: "infrastructure",
|
|
8616
10243
|
amba_sdk_get_setup_instructions: "infrastructure",
|
|
8617
10244
|
amba_get_sdk_setup_instructions: "infrastructure",
|
|
8618
10245
|
amba_billing_status: "infrastructure",
|
|
8619
10246
|
amba_billing_tiers: "infrastructure",
|
|
8620
10247
|
amba_billing_set_ceiling: "infrastructure",
|
|
10248
|
+
amba_payments_account_create: "economy",
|
|
10249
|
+
amba_payments_create_onboarding_link: "economy",
|
|
10250
|
+
amba_payments_account_status: "economy",
|
|
10251
|
+
amba_payments_set_fee: "economy",
|
|
10252
|
+
amba_payments_charge: "economy",
|
|
10253
|
+
amba_payments_refund: "economy",
|
|
10254
|
+
amba_payments_balance: "economy",
|
|
10255
|
+
amba_payments_payouts: "economy",
|
|
10256
|
+
amba_agent_checkout_enable: "economy",
|
|
10257
|
+
amba_agent_checkout_capabilities: "economy",
|
|
8621
10258
|
amba_projects_invite_member: "infrastructure",
|
|
8622
10259
|
amba_projects_list_members: "infrastructure",
|
|
8623
10260
|
amba_projects_remove_member: "infrastructure",
|
|
@@ -8651,7 +10288,10 @@ const TOOL_CATEGORY = {
|
|
|
8651
10288
|
amba_remote_configs_create: "infrastructure",
|
|
8652
10289
|
amba_remote_configs_list: "infrastructure",
|
|
8653
10290
|
amba_remote_configs_update: "infrastructure",
|
|
8654
|
-
amba_remote_configs_delete: "infrastructure"
|
|
10291
|
+
amba_remote_configs_delete: "infrastructure",
|
|
10292
|
+
amba_app_mcp_get_config: "infrastructure",
|
|
10293
|
+
amba_app_mcp_update_config: "infrastructure",
|
|
10294
|
+
amba_app_mcp_set_exposure: "infrastructure"
|
|
8655
10295
|
};
|
|
8656
10296
|
/**
|
|
8657
10297
|
* Look up the canonical category for an MCP tool name. Accepts both
|
|
@@ -8664,6 +10304,928 @@ function getToolCategory(toolName) {
|
|
|
8664
10304
|
return TOOL_CATEGORY[toolName] ?? null;
|
|
8665
10305
|
}
|
|
8666
10306
|
//#endregion
|
|
10307
|
+
//#region src/auto/schema-to-zod.ts
|
|
10308
|
+
/**
|
|
10309
|
+
* Pure mapper: a collection's live schema (the JSON returned by the
|
|
10310
|
+
* `GET .../collections/:name` describe endpoint) → the zod raw shapes the
|
|
10311
|
+
* auto-generated MCP tools register.
|
|
10312
|
+
*
|
|
10313
|
+
* Three derived shapes per collection (the auto-MCP wedge §2.2):
|
|
10314
|
+
*
|
|
10315
|
+
* - `insertShape` — the typed body of an `insert` tool. Server-managed
|
|
10316
|
+
* columns (`id`, `user_id`, `created_at`, `updated_at`, `deleted_at`)
|
|
10317
|
+
* are omitted (the route stamps them); a column is REQUIRED when it is
|
|
10318
|
+
* NOT NULL and has no default, OPTIONAL otherwise.
|
|
10319
|
+
* - `whereShape` — the typed `where` of a `find` tool: one optional key
|
|
10320
|
+
* per non-vector column whose value is `value | { eq?, ne?, gt?, … }`
|
|
10321
|
+
* typed to the column. This is the single highest-value projection —
|
|
10322
|
+
* it makes the generated tool read like a hand-built typed client
|
|
10323
|
+
* rather than a free-form `where: object` blob.
|
|
10324
|
+
* - `updateShape` — the typed `set` of an `update` tool: every non
|
|
10325
|
+
* server-managed column, all optional (merge-patch semantics).
|
|
10326
|
+
*
|
|
10327
|
+
* Plus `rowTypeHints` — a column→plain-English type label map the
|
|
10328
|
+
* description generator (`describe.ts`) reads, and `vectorColumns` — the
|
|
10329
|
+
* vector columns (excluded from row input; they drive `find_nearest`).
|
|
10330
|
+
*
|
|
10331
|
+
* The mapper is PURE (no I/O, no zod-runtime coupling beyond building the
|
|
10332
|
+
* shape) so it unit-tests in isolation and so the same projection can run
|
|
10333
|
+
* in the hosted server today and in the CLI / console later.
|
|
10334
|
+
*/
|
|
10335
|
+
/**
|
|
10336
|
+
* Columns Amba manages on every collection. They are stamped by the
|
|
10337
|
+
* route, never accepted as tool input, and so are omitted from the
|
|
10338
|
+
* insert / update shapes. They ARE filterable (`where`) and selectable.
|
|
10339
|
+
*/
|
|
10340
|
+
const SERVER_MANAGED_COLUMNS = new Set([
|
|
10341
|
+
"id",
|
|
10342
|
+
"user_id",
|
|
10343
|
+
"created_at",
|
|
10344
|
+
"updated_at",
|
|
10345
|
+
"deleted_at"
|
|
10346
|
+
]);
|
|
10347
|
+
/**
|
|
10348
|
+
* Classify a column into a coarse {@link ColumnKind}. Reads `data_type`
|
|
10349
|
+
* first (the only reliable array signal — `data_type === 'ARRAY'`), then
|
|
10350
|
+
* `udt_name`. Unknown types fall back to `unknown` (mapped to free JSON)
|
|
10351
|
+
* rather than throwing, so a future column type never breaks generation.
|
|
10352
|
+
*/
|
|
10353
|
+
function classifyColumn(column) {
|
|
10354
|
+
const udt = column.udt_name.toLowerCase();
|
|
10355
|
+
if (column.data_type.toUpperCase() === "ARRAY" || udt.startsWith("_")) switch (udt.replace(/^_/, "")) {
|
|
10356
|
+
case "text":
|
|
10357
|
+
case "varchar":
|
|
10358
|
+
case "bpchar": return "text[]";
|
|
10359
|
+
case "int2":
|
|
10360
|
+
case "int4":
|
|
10361
|
+
case "int8": return "integer[]";
|
|
10362
|
+
case "numeric":
|
|
10363
|
+
case "float4":
|
|
10364
|
+
case "float8": return "number[]";
|
|
10365
|
+
case "bool": return "boolean[]";
|
|
10366
|
+
case "uuid": return "uuid[]";
|
|
10367
|
+
default: return "unknown";
|
|
10368
|
+
}
|
|
10369
|
+
switch (udt) {
|
|
10370
|
+
case "text":
|
|
10371
|
+
case "varchar":
|
|
10372
|
+
case "bpchar": return "text";
|
|
10373
|
+
case "int2":
|
|
10374
|
+
case "int4":
|
|
10375
|
+
case "int8": return "integer";
|
|
10376
|
+
case "numeric":
|
|
10377
|
+
case "float4":
|
|
10378
|
+
case "float8": return "number";
|
|
10379
|
+
case "bool": return "boolean";
|
|
10380
|
+
case "jsonb":
|
|
10381
|
+
case "json": return "json";
|
|
10382
|
+
case "uuid": return "uuid";
|
|
10383
|
+
case "date": return "date";
|
|
10384
|
+
case "timestamptz":
|
|
10385
|
+
case "timestamp": return "timestamp";
|
|
10386
|
+
case "vector": return "vector";
|
|
10387
|
+
default: return "unknown";
|
|
10388
|
+
}
|
|
10389
|
+
}
|
|
10390
|
+
/** Build the base (scalar/array) zod type for a column kind. */
|
|
10391
|
+
function baseTypeForKind(kind) {
|
|
10392
|
+
switch (kind) {
|
|
10393
|
+
case "text": return z.string();
|
|
10394
|
+
case "uuid": return z.string().describe("uuid");
|
|
10395
|
+
case "date": return z.string().describe("ISO-8601 date (YYYY-MM-DD)");
|
|
10396
|
+
case "timestamp": return z.string().describe("ISO-8601 timestamp");
|
|
10397
|
+
case "integer": return z.number().int();
|
|
10398
|
+
case "number": return z.number();
|
|
10399
|
+
case "boolean": return z.boolean();
|
|
10400
|
+
case "json": return z.unknown();
|
|
10401
|
+
case "text[]": return z.array(z.string());
|
|
10402
|
+
case "integer[]": return z.array(z.number().int());
|
|
10403
|
+
case "number[]": return z.array(z.number());
|
|
10404
|
+
case "boolean[]": return z.array(z.boolean());
|
|
10405
|
+
case "uuid[]": return z.array(z.string());
|
|
10406
|
+
case "vector": return z.array(z.number());
|
|
10407
|
+
default: return z.unknown();
|
|
10408
|
+
}
|
|
10409
|
+
}
|
|
10410
|
+
/**
|
|
10411
|
+
* Build the per-column `where` operator object. Every operator is
|
|
10412
|
+
* optional; the value type matches the column. A bare value is also
|
|
10413
|
+
* accepted as shorthand for `{ eq: value }` (mirrors the server, which
|
|
10414
|
+
* treats a non-object field value as equality).
|
|
10415
|
+
*/
|
|
10416
|
+
function whereTypeForKind(kind, columnName) {
|
|
10417
|
+
const base = baseTypeForKind(kind);
|
|
10418
|
+
const isArray = kind.endsWith("[]");
|
|
10419
|
+
const isComparable = kind === "integer" || kind === "number" || kind === "date" || kind === "timestamp" || kind === "text";
|
|
10420
|
+
const isText = kind === "text";
|
|
10421
|
+
const ops = {
|
|
10422
|
+
eq: base.optional(),
|
|
10423
|
+
ne: base.optional(),
|
|
10424
|
+
in: z.array(base).optional(),
|
|
10425
|
+
notIn: z.array(base).optional(),
|
|
10426
|
+
isNull: z.boolean().optional(),
|
|
10427
|
+
isNotNull: z.boolean().optional()
|
|
10428
|
+
};
|
|
10429
|
+
if (isComparable) {
|
|
10430
|
+
ops.gt = base.optional();
|
|
10431
|
+
ops.gte = base.optional();
|
|
10432
|
+
ops.lt = base.optional();
|
|
10433
|
+
ops.lte = base.optional();
|
|
10434
|
+
}
|
|
10435
|
+
if (isText) {
|
|
10436
|
+
ops.like = z.string().optional().describe("SQL LIKE pattern (case-sensitive).");
|
|
10437
|
+
ops.ilike = z.string().optional().describe("SQL ILIKE pattern (case-insensitive).");
|
|
10438
|
+
}
|
|
10439
|
+
if (isArray) {
|
|
10440
|
+
ops.contains = base.optional().describe("Rows whose array contains all of these elements.");
|
|
10441
|
+
ops.containedBy = base.optional().describe("Rows whose array is contained by these elements.");
|
|
10442
|
+
ops.overlaps = base.optional().describe("Rows whose array shares any element with these.");
|
|
10443
|
+
}
|
|
10444
|
+
return z.union([z.object(ops), base]).optional().describe(`Filter on \`${columnName}\`. Bare value = equality; or an operator object.`);
|
|
10445
|
+
}
|
|
10446
|
+
/**
|
|
10447
|
+
* Whether a column is required on insert: NOT NULL and no column default.
|
|
10448
|
+
* A defaulted NOT NULL column is optional (the DB fills it).
|
|
10449
|
+
*/
|
|
10450
|
+
function isRequiredOnInsert(column) {
|
|
10451
|
+
return column.is_nullable === "NO" && column.column_default === null;
|
|
10452
|
+
}
|
|
10453
|
+
/**
|
|
10454
|
+
* Project a collection schema into the zod raw shapes the auto-MCP tools
|
|
10455
|
+
* register. Pure — same input always yields the same shapes.
|
|
10456
|
+
*/
|
|
10457
|
+
function mapCollectionSchema(schema, options = {}) {
|
|
10458
|
+
const insertShape = {};
|
|
10459
|
+
const whereShape = {};
|
|
10460
|
+
const updateShape = {};
|
|
10461
|
+
const rowTypeHints = {};
|
|
10462
|
+
const vectorColumns = [];
|
|
10463
|
+
for (const column of schema.columns) {
|
|
10464
|
+
const name = column.column_name;
|
|
10465
|
+
const kind = classifyColumn(column);
|
|
10466
|
+
rowTypeHints[name] = kind;
|
|
10467
|
+
if (kind === "vector") {
|
|
10468
|
+
vectorColumns.push(name);
|
|
10469
|
+
continue;
|
|
10470
|
+
}
|
|
10471
|
+
whereShape[name] = whereTypeForKind(kind, name);
|
|
10472
|
+
if (SERVER_MANAGED_COLUMNS.has(name)) {
|
|
10473
|
+
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.");
|
|
10474
|
+
continue;
|
|
10475
|
+
}
|
|
10476
|
+
const base = baseTypeForKind(kind);
|
|
10477
|
+
updateShape[name] = base.optional();
|
|
10478
|
+
insertShape[name] = isRequiredOnInsert(column) ? base : base.optional();
|
|
10479
|
+
}
|
|
10480
|
+
return {
|
|
10481
|
+
insertShape,
|
|
10482
|
+
whereShape,
|
|
10483
|
+
updateShape,
|
|
10484
|
+
rowTypeHints,
|
|
10485
|
+
vectorColumns,
|
|
10486
|
+
hasVector: vectorColumns.length > 0
|
|
10487
|
+
};
|
|
10488
|
+
}
|
|
10489
|
+
//#endregion
|
|
10490
|
+
//#region src/auto/describe.ts
|
|
10491
|
+
/**
|
|
10492
|
+
* SDK-grade per-tool description generator for the auto-MCP wedge.
|
|
10493
|
+
*
|
|
10494
|
+
* The wedge is only worth shipping if the generated tools read like a
|
|
10495
|
+
* hand-built typed client, not a thin `find_rows({collection, where})`
|
|
10496
|
+
* wrapper (design doc §7). These descriptions are generated from the live
|
|
10497
|
+
* column list + access policy so an agent never needs external docs:
|
|
10498
|
+
* column names, types, nullability, the access policy in plain words, and
|
|
10499
|
+
* a worked example per verb.
|
|
10500
|
+
*
|
|
10501
|
+
* Pure string builders — snapshot-friendly, no I/O. Collections are named as
|
|
10502
|
+
* what they are — relational Postgres tables. The hosting supplier (Neon) and
|
|
10503
|
+
* the rest of the infra stack stay hidden: "collection", "row", "column",
|
|
10504
|
+
* and "function" are the customer vocabulary, never a supplier name.
|
|
10505
|
+
*/
|
|
10506
|
+
/** A human-friendly type label for a column kind. */
|
|
10507
|
+
function labelForKind(kind) {
|
|
10508
|
+
switch (kind) {
|
|
10509
|
+
case "integer": return "integer";
|
|
10510
|
+
case "number": return "number";
|
|
10511
|
+
case "boolean": return "boolean";
|
|
10512
|
+
case "json": return "JSON";
|
|
10513
|
+
case "uuid": return "uuid (string)";
|
|
10514
|
+
case "date": return "date string";
|
|
10515
|
+
case "timestamp": return "timestamp string";
|
|
10516
|
+
case "vector": return "vector";
|
|
10517
|
+
case "text[]": return "string array";
|
|
10518
|
+
case "integer[]": return "integer array";
|
|
10519
|
+
case "number[]": return "number array";
|
|
10520
|
+
case "boolean[]": return "boolean array";
|
|
10521
|
+
case "uuid[]": return "uuid array";
|
|
10522
|
+
case "text": return "text";
|
|
10523
|
+
default: return "value";
|
|
10524
|
+
}
|
|
10525
|
+
}
|
|
10526
|
+
/**
|
|
10527
|
+
* One-line summary of the collection's columns an agent can write to
|
|
10528
|
+
* (insert/update). Server-managed columns are excluded — the route stamps
|
|
10529
|
+
* them.
|
|
10530
|
+
*/
|
|
10531
|
+
function writableColumnSummary(mapped) {
|
|
10532
|
+
const cols = Object.keys(mapped.rowTypeHints).filter((c) => !SERVER_MANAGED_COLUMNS.has(c) && mapped.rowTypeHints[c] !== "vector");
|
|
10533
|
+
if (cols.length === 0) return "(no writable columns)";
|
|
10534
|
+
return cols.map((c) => `${c} (${labelForKind(mapped.rowTypeHints[c])})`).join(", ");
|
|
10535
|
+
}
|
|
10536
|
+
/** All columns with types, for the find/list filter description. */
|
|
10537
|
+
function allColumnSummary(mapped) {
|
|
10538
|
+
const cols = Object.keys(mapped.rowTypeHints);
|
|
10539
|
+
if (cols.length === 0) return "(no columns)";
|
|
10540
|
+
return cols.map((c) => `${c} (${labelForKind(mapped.rowTypeHints[c])})`).join(", ");
|
|
10541
|
+
}
|
|
10542
|
+
/** Plain-words access-policy sentence for the given scope. */
|
|
10543
|
+
function policySentence(ctx) {
|
|
10544
|
+
if (ctx.scope === "admin") return "Developer/admin scope: sees and mutates EVERY row regardless of which app user owns it (no per-user scoping).";
|
|
10545
|
+
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."}`;
|
|
10546
|
+
}
|
|
10547
|
+
/** Build the first writable column name for worked examples. */
|
|
10548
|
+
function exampleWritableColumn(mapped) {
|
|
10549
|
+
return Object.keys(mapped.rowTypeHints).find((c) => !SERVER_MANAGED_COLUMNS.has(c) && mapped.rowTypeHints[c] !== "vector") ?? null;
|
|
10550
|
+
}
|
|
10551
|
+
/** Example value literal for a column kind (for worked examples). */
|
|
10552
|
+
function exampleValue(kind) {
|
|
10553
|
+
switch (kind) {
|
|
10554
|
+
case "integer": return "42";
|
|
10555
|
+
case "number": return "3.14";
|
|
10556
|
+
case "boolean": return "true";
|
|
10557
|
+
case "json": return "{\"key\":\"value\"}";
|
|
10558
|
+
case "uuid": return "\"00000000-0000-0000-0000-000000000000\"";
|
|
10559
|
+
case "date": return "\"2026-01-31\"";
|
|
10560
|
+
case "timestamp": return "\"2026-01-31T12:00:00Z\"";
|
|
10561
|
+
case "text[]": return "[\"a\",\"b\"]";
|
|
10562
|
+
case "integer[]": return "[1,2,3]";
|
|
10563
|
+
case "number[]": return "[1.5,2.5]";
|
|
10564
|
+
case "boolean[]": return "[true,false]";
|
|
10565
|
+
case "uuid[]": return "[\"00000000-0000-0000-0000-000000000000\"]";
|
|
10566
|
+
default: return "\"example\"";
|
|
10567
|
+
}
|
|
10568
|
+
}
|
|
10569
|
+
/**
|
|
10570
|
+
* Generate the description string for a single collection tool.
|
|
10571
|
+
*/
|
|
10572
|
+
function describeCollectionTool(verb, ctx) {
|
|
10573
|
+
const { collection, mapped } = ctx;
|
|
10574
|
+
const policy = policySentence(ctx);
|
|
10575
|
+
const exampleCol = exampleWritableColumn(mapped);
|
|
10576
|
+
const exampleKind = exampleCol ? mapped.rowTypeHints[exampleCol] : "text";
|
|
10577
|
+
switch (verb) {
|
|
10578
|
+
case "list": return [
|
|
10579
|
+
`List rows in the "${collection}" collection, newest first.`,
|
|
10580
|
+
`Returns a page of rows plus a \`next_cursor\` — pass it back as \`cursor\` to fetch the next page.`,
|
|
10581
|
+
`Columns: ${allColumnSummary(mapped)}.`,
|
|
10582
|
+
policy,
|
|
10583
|
+
`Example: { "limit": 20 }.`
|
|
10584
|
+
].join(" ");
|
|
10585
|
+
case "get": return [
|
|
10586
|
+
`Fetch one row from "${collection}" by its \`id\`.`,
|
|
10587
|
+
`Returns the full row, or a 404 if no row with that id exists (or it is outside your scope).`,
|
|
10588
|
+
policy,
|
|
10589
|
+
`Example: { "id": "00000000-0000-0000-0000-000000000000" }.`
|
|
10590
|
+
].join(" ");
|
|
10591
|
+
case "find": return [
|
|
10592
|
+
`Find rows in "${collection}" with a typed, per-column filter.`,
|
|
10593
|
+
`Each \`where\` key is a column; its value is a bare value (equality) or an operator object`,
|
|
10594
|
+
`({ eq, ne, gt, gte, lt, lte, in, notIn, like, ilike, isNull, isNotNull, contains, containedBy, overlaps }).`,
|
|
10595
|
+
`Combine with top-level \`and\`/\`or\`/\`not\`. Supports \`order\`, \`limit\`, and \`cursor\` pagination (returns \`next_cursor\`).`,
|
|
10596
|
+
`Columns: ${allColumnSummary(mapped)}.`,
|
|
10597
|
+
policy,
|
|
10598
|
+
exampleCol ? `Example: { "where": { "${exampleCol}": { "eq": ${exampleValue(exampleKind)} } }, "limit": 20 }.` : `Example: { "where": {}, "limit": 20 }.`
|
|
10599
|
+
].join(" ");
|
|
10600
|
+
case "insert": return [
|
|
10601
|
+
`Insert a new row into "${collection}".`,
|
|
10602
|
+
`Provide values for the writable columns: ${writableColumnSummary(mapped)}.`,
|
|
10603
|
+
`Server-managed columns (id, user_id, created_at, updated_at, deleted_at) are stamped automatically — do not send them.`,
|
|
10604
|
+
policy,
|
|
10605
|
+
exampleCol ? `Example: { "${exampleCol}": ${exampleValue(exampleKind)} }.` : `Example: {}.`
|
|
10606
|
+
].join(" ");
|
|
10607
|
+
case "update": return [
|
|
10608
|
+
`Update an existing row in "${collection}" by \`id\` (merge-patch — only the fields you send change).`,
|
|
10609
|
+
`Writable columns: ${writableColumnSummary(mapped)}.`,
|
|
10610
|
+
`Server-managed columns are rejected.`,
|
|
10611
|
+
policy,
|
|
10612
|
+
exampleCol ? `Example: { "id": "…", "set": { "${exampleCol}": ${exampleValue(exampleKind)} } }.` : `Example: { "id": "…", "set": {} }.`
|
|
10613
|
+
].join(" ");
|
|
10614
|
+
case "delete": return [
|
|
10615
|
+
`Delete a row from "${collection}" by \`id\`.`,
|
|
10616
|
+
`Soft-delete by default (recoverable); pass \`hard: true\` for a permanent removal.`,
|
|
10617
|
+
policy,
|
|
10618
|
+
`Example: { "id": "00000000-0000-0000-0000-000000000000" }.`
|
|
10619
|
+
].join(" ");
|
|
10620
|
+
case "aggregate": return [
|
|
10621
|
+
`Compute aggregates over "${collection}": count / sum / avg / min / max, optionally grouped by columns.`,
|
|
10622
|
+
`\`count\` with no column = COUNT(*). Combine with a typed \`where\` to aggregate a subset.`,
|
|
10623
|
+
`Columns: ${allColumnSummary(mapped)}.`,
|
|
10624
|
+
policy,
|
|
10625
|
+
`Example: { "select": [{ "fn": "count" }] }.`
|
|
10626
|
+
].join(" ");
|
|
10627
|
+
case "find_nearest": return [
|
|
10628
|
+
`Vector similarity search over "${collection}" — returns the rows closest to your query, with a synthesized \`_distance\` column.`,
|
|
10629
|
+
`This tool exists because "${collection}" has a vector column (${mapped.vectorColumns.join(", ")}).`,
|
|
10630
|
+
`Pass \`to_text\` (the server computes the embedding) or a raw \`to_vector\`; optionally narrow with a typed \`where\`.`,
|
|
10631
|
+
policy,
|
|
10632
|
+
`Example: { "to_text": "cozy winter recipes", "k": 5 }.`
|
|
10633
|
+
].join(" ");
|
|
10634
|
+
default: return `Operate on the "${collection}" collection.`;
|
|
10635
|
+
}
|
|
10636
|
+
}
|
|
10637
|
+
/** Description for the `<ns>_list_collections` meta tool. */
|
|
10638
|
+
function describeListCollections(namespace) {
|
|
10639
|
+
return [
|
|
10640
|
+
`List every collection exposed by this app's agent surface (namespace "${namespace}").`,
|
|
10641
|
+
`Each collection is a relational Postgres table — typed columns, foreign keys, transactions, unique indexes, and vector search —`,
|
|
10642
|
+
`projected into typed list/get/find/insert/update/delete/aggregate tools you can call directly.`,
|
|
10643
|
+
`This tool is the index. Returns each collection's name and column summary.`
|
|
10644
|
+
].join(" ");
|
|
10645
|
+
}
|
|
10646
|
+
/**
|
|
10647
|
+
* Generate the description string for a function tool. Honest-confidence
|
|
10648
|
+
* rule: a tool with a declared schema reads like a hand-built SDK call; a
|
|
10649
|
+
* tool WITHOUT one says so plainly instead of over-claiming.
|
|
10650
|
+
*/
|
|
10651
|
+
function describeFunctionTool(ctx) {
|
|
10652
|
+
const parts = [];
|
|
10653
|
+
if (ctx.declaredDescription) {
|
|
10654
|
+
parts.push(ctx.declaredDescription.trim());
|
|
10655
|
+
parts.push(`(Deployed function "${ctx.fn}" on this app.)`);
|
|
10656
|
+
} else parts.push(`Invoke the deployed function "${ctx.fn}" on this app.`);
|
|
10657
|
+
if (ctx.hasDeclaredSchema) parts.push("Arguments match the input schema the function declared at deploy time and are sent as the request body.");
|
|
10658
|
+
else parts.push("This function declared no input schema — pass `body` (free-form JSON sent as the request body) and/or `query` (string key/value pairs appended to the URL). Consult the function author for the expected shape.");
|
|
10659
|
+
parts.push("Runs with the same credential as this connection; the response status and body are returned as-is.");
|
|
10660
|
+
return parts.join(" ");
|
|
10661
|
+
}
|
|
10662
|
+
/** Description for the `<ns>_whoami` meta tool. */
|
|
10663
|
+
function describeWhoami(namespace) {
|
|
10664
|
+
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(" ");
|
|
10665
|
+
}
|
|
10666
|
+
//#endregion
|
|
10667
|
+
//#region src/auto/collection-tools.ts
|
|
10668
|
+
async function proxyFetch(baseUrl, headers, call) {
|
|
10669
|
+
let url = `${baseUrl}${call.path}`;
|
|
10670
|
+
if (call.query && Object.keys(call.query).length > 0) url += `?${new URLSearchParams(call.query).toString()}`;
|
|
10671
|
+
const allHeaders = {
|
|
10672
|
+
Accept: "application/json",
|
|
10673
|
+
...headers
|
|
10674
|
+
};
|
|
10675
|
+
if (call.body !== void 0) allHeaders["Content-Type"] = "application/json";
|
|
10676
|
+
const res = await fetch(url, {
|
|
10677
|
+
method: call.method,
|
|
10678
|
+
headers: allHeaders,
|
|
10679
|
+
body: call.body !== void 0 ? JSON.stringify(call.body) : void 0
|
|
10680
|
+
});
|
|
10681
|
+
let body;
|
|
10682
|
+
try {
|
|
10683
|
+
body = await res.json();
|
|
10684
|
+
} catch {
|
|
10685
|
+
body = { error: {
|
|
10686
|
+
code: "INVALID_RESPONSE",
|
|
10687
|
+
message: res.statusText
|
|
10688
|
+
} };
|
|
10689
|
+
}
|
|
10690
|
+
return {
|
|
10691
|
+
status: res.status,
|
|
10692
|
+
body
|
|
10693
|
+
};
|
|
10694
|
+
}
|
|
10695
|
+
/** Admin-route fetch (mirrors tools/collections.ts adminFetch). */
|
|
10696
|
+
async function adminFetch(apiClient, bearer, call) {
|
|
10697
|
+
return proxyFetch(apiClient.getBaseUrl(), { Authorization: `Bearer ${bearer}` }, call);
|
|
10698
|
+
}
|
|
10699
|
+
/** Client-route fetch: API key + optional end-user session Bearer. */
|
|
10700
|
+
async function clientFetch(clientAuth, call) {
|
|
10701
|
+
const headers = { "X-Api-Key": clientAuth.apiKey };
|
|
10702
|
+
if (clientAuth.sessionToken) headers["Authorization"] = `Bearer ${clientAuth.sessionToken}`;
|
|
10703
|
+
return proxyFetch(clientAuth.baseUrl, headers, call);
|
|
10704
|
+
}
|
|
10705
|
+
/**
|
|
10706
|
+
* Tool-name fragment regex. MCP + `tool-naming.test.ts` require lowercase
|
|
10707
|
+
* `_`-separated names. The namespace + collection are sanitized to that
|
|
10708
|
+
* shape so a slug like "My App" or a collection like "MyRecipes" never
|
|
10709
|
+
* produces an invalid tool name.
|
|
10710
|
+
*/
|
|
10711
|
+
function sanitizeNameFragment(raw) {
|
|
10712
|
+
const cleaned = raw.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
10713
|
+
return /^[a-z]/.test(cleaned) ? cleaned : `c_${cleaned}`;
|
|
10714
|
+
}
|
|
10715
|
+
/** Build the `<ns>_<coll>_<verb>` tool name. */
|
|
10716
|
+
function collectionToolName(namespace, collection, verb) {
|
|
10717
|
+
return `${sanitizeNameFragment(namespace)}_${sanitizeNameFragment(collection)}_${verb}`;
|
|
10718
|
+
}
|
|
10719
|
+
/**
|
|
10720
|
+
* Default `order` for list/find when the caller gives none. A
|
|
10721
|
+
* deterministic single-column order is what lets the route return a
|
|
10722
|
+
* bootstrap `next_cursor` on the first page (and is what the tool
|
|
10723
|
+
* description promises: newest first).
|
|
10724
|
+
*/
|
|
10725
|
+
const DEFAULT_LIST_ORDER = "created_at desc";
|
|
10726
|
+
/**
|
|
10727
|
+
* Register the typed CRUD tools for a single collection on `server`,
|
|
10728
|
+
* proxying to the existing authoritative routes for the requested scope.
|
|
10729
|
+
*/
|
|
10730
|
+
function registerCollectionToolsFor(server, apiClient, options) {
|
|
10731
|
+
const { projectId, namespace, collection, schema, policy, scope, clientAuth } = options;
|
|
10732
|
+
if (scope === "client" && !clientAuth) throw new Error("registerCollectionToolsFor: clientAuth is required for client scope");
|
|
10733
|
+
const mapped = mapCollectionSchema(schema, { includeUserIdInInsert: scope === "admin" });
|
|
10734
|
+
const describeCtx = {
|
|
10735
|
+
collection,
|
|
10736
|
+
mapped,
|
|
10737
|
+
readPolicy: policy.read_policy,
|
|
10738
|
+
writePolicy: policy.write_policy,
|
|
10739
|
+
scope
|
|
10740
|
+
};
|
|
10741
|
+
const encColl = encodeURIComponent(collection);
|
|
10742
|
+
const rowsRoot = scope === "admin" ? `/projects/${projectId}/collections/${encColl}/rows` : `/collections/${encColl}`;
|
|
10743
|
+
const name = (verb) => collectionToolName(namespace, collection, verb);
|
|
10744
|
+
/**
|
|
10745
|
+
* Register one verb tool. Admin scope goes through `registerTool` (the
|
|
10746
|
+
* pat-as-argument helper — the developer Bearer flows from the inbound
|
|
10747
|
+
* request or a per-call `pat` arg); client scope registers a plain tool
|
|
10748
|
+
* whose handler closes over the connection's API key + session token
|
|
10749
|
+
* (an end-user agent has no pat — its credential IS the connection).
|
|
10750
|
+
*/
|
|
10751
|
+
function addTool(verb, schemaShape, run) {
|
|
10752
|
+
if (scope === "client") {
|
|
10753
|
+
registerPublicTool(server, name(verb), describeCollectionTool(verb, describeCtx), schemaShape, async (args) => run(args, (call) => clientFetch(clientAuth, call)));
|
|
10754
|
+
return;
|
|
10755
|
+
}
|
|
10756
|
+
registerTool(server, apiClient, name(verb), describeCollectionTool(verb, describeCtx), schemaShape, async (args, { pat }) => run(args, (call) => adminFetch(apiClient, pat, call)));
|
|
10757
|
+
}
|
|
10758
|
+
/** Build the `?query=` JSON for list/find from common args. */
|
|
10759
|
+
function buildFindQuery(args) {
|
|
10760
|
+
const findQuery = {};
|
|
10761
|
+
if (args.where !== void 0) findQuery.where = args.where;
|
|
10762
|
+
findQuery.order = args.order !== void 0 ? args.order : DEFAULT_LIST_ORDER;
|
|
10763
|
+
if (args.limit !== void 0) findQuery.limit = args.limit;
|
|
10764
|
+
if (args.cursor !== void 0) findQuery.cursor = args.cursor;
|
|
10765
|
+
if (args.select !== void 0) findQuery.select = args.select;
|
|
10766
|
+
if (args.include_deleted !== void 0) findQuery.includeDeleted = args.include_deleted;
|
|
10767
|
+
return findQuery;
|
|
10768
|
+
}
|
|
10769
|
+
const listFindCommon = {
|
|
10770
|
+
order: z.union([z.string(), z.array(z.string())]).optional().describe("Order spec, e.g. \"created_at desc\". Defaults to \"created_at desc\"."),
|
|
10771
|
+
limit: z.number().int().positive().optional().describe("Max rows (default 50)."),
|
|
10772
|
+
cursor: z.string().optional().describe("Opaque cursor from a prior page (`next_cursor`). Omit for the first page."),
|
|
10773
|
+
select: z.array(z.string()).optional().describe("Subset of columns to return."),
|
|
10774
|
+
include_deleted: z.boolean().optional().describe("Include soft-deleted rows. Default false.")
|
|
10775
|
+
};
|
|
10776
|
+
addTool("list", { ...listFindCommon }, async (args, exec) => {
|
|
10777
|
+
const findQuery = buildFindQuery(args);
|
|
10778
|
+
return passthroughResult(await exec({
|
|
10779
|
+
method: "GET",
|
|
10780
|
+
path: rowsRoot,
|
|
10781
|
+
query: { query: JSON.stringify(findQuery) }
|
|
10782
|
+
}));
|
|
10783
|
+
});
|
|
10784
|
+
addTool("get", { id: z.string().describe("Row id (uuid).") }, async (args, exec) => {
|
|
10785
|
+
return passthroughResult(await exec({
|
|
10786
|
+
method: "GET",
|
|
10787
|
+
path: `${rowsRoot}/${encodeURIComponent(String(args.id))}`
|
|
10788
|
+
}));
|
|
10789
|
+
});
|
|
10790
|
+
addTool("find", {
|
|
10791
|
+
where: z.object({
|
|
10792
|
+
...mapped.whereShape,
|
|
10793
|
+
and: z.array(z.unknown()).optional().describe("AND-combine nested where clauses."),
|
|
10794
|
+
or: z.array(z.unknown()).optional().describe("OR-combine nested where clauses."),
|
|
10795
|
+
not: z.unknown().optional().describe("Negate a nested where clause.")
|
|
10796
|
+
}).partial().optional().describe("Typed, per-column filter."),
|
|
10797
|
+
...listFindCommon
|
|
10798
|
+
}, async (args, exec) => {
|
|
10799
|
+
const findQuery = buildFindQuery(args);
|
|
10800
|
+
return passthroughResult(await exec({
|
|
10801
|
+
method: "GET",
|
|
10802
|
+
path: rowsRoot,
|
|
10803
|
+
query: { query: JSON.stringify(findQuery) }
|
|
10804
|
+
}));
|
|
10805
|
+
});
|
|
10806
|
+
addTool("insert", {
|
|
10807
|
+
...mapped.insertShape,
|
|
10808
|
+
...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.") } : {}
|
|
10809
|
+
}, async (args, exec) => {
|
|
10810
|
+
const row = {};
|
|
10811
|
+
for (const [k, v] of Object.entries(args)) {
|
|
10812
|
+
if (k === "pat") continue;
|
|
10813
|
+
if (v !== void 0) row[k] = v;
|
|
10814
|
+
}
|
|
10815
|
+
return passthroughResult(await exec({
|
|
10816
|
+
method: "POST",
|
|
10817
|
+
path: rowsRoot,
|
|
10818
|
+
body: row
|
|
10819
|
+
}));
|
|
10820
|
+
});
|
|
10821
|
+
addTool("update", {
|
|
10822
|
+
id: z.string().describe("Row id (uuid) to update."),
|
|
10823
|
+
set: z.object({ ...mapped.updateShape }).describe("Column→value map of the fields to change (merge-patch).")
|
|
10824
|
+
}, async (args, exec) => {
|
|
10825
|
+
return passthroughResult(await exec({
|
|
10826
|
+
method: "PATCH",
|
|
10827
|
+
path: `${rowsRoot}/${encodeURIComponent(String(args.id))}`,
|
|
10828
|
+
body: { set: args.set }
|
|
10829
|
+
}));
|
|
10830
|
+
});
|
|
10831
|
+
addTool("delete", {
|
|
10832
|
+
id: z.string().describe("Row id (uuid) to delete."),
|
|
10833
|
+
...scope === "admin" ? { hard: z.boolean().optional().describe("false (default) soft-deletes (recoverable); true permanently removes.") } : {}
|
|
10834
|
+
}, async (args, exec) => {
|
|
10835
|
+
return passthroughResult(await exec({
|
|
10836
|
+
method: "DELETE",
|
|
10837
|
+
path: `${rowsRoot}/${encodeURIComponent(String(args.id))}`,
|
|
10838
|
+
query: scope === "admin" && args.hard === true ? { hard: "true" } : void 0
|
|
10839
|
+
}));
|
|
10840
|
+
});
|
|
10841
|
+
addTool("aggregate", {
|
|
10842
|
+
select: z.array(z.object({
|
|
10843
|
+
fn: z.enum([
|
|
10844
|
+
"count",
|
|
10845
|
+
"sum",
|
|
10846
|
+
"avg",
|
|
10847
|
+
"min",
|
|
10848
|
+
"max"
|
|
10849
|
+
]),
|
|
10850
|
+
column: z.string().optional().describe("Column to aggregate. Required for sum/avg/min/max; omit for count(*)."),
|
|
10851
|
+
as: z.string().optional().describe("Output alias.")
|
|
10852
|
+
})).min(1).describe("One or more aggregations to compute."),
|
|
10853
|
+
group_by: z.union([z.string(), z.array(z.string())]).optional().describe("Column(s) to group by. Omit for a whole-collection aggregate."),
|
|
10854
|
+
where: z.object({ ...mapped.whereShape }).partial().optional().describe("Typed, per-column filter to aggregate a subset."),
|
|
10855
|
+
include_deleted: z.boolean().optional().describe("Include soft-deleted rows. Default false.")
|
|
10856
|
+
}, async (args, exec) => {
|
|
10857
|
+
const body = { select: args.select };
|
|
10858
|
+
if (args.group_by !== void 0) body.group_by = args.group_by;
|
|
10859
|
+
if (args.where !== void 0) body.where = args.where;
|
|
10860
|
+
if (args.include_deleted !== void 0) if (scope === "admin") body.includeDeleted = args.include_deleted;
|
|
10861
|
+
else body.include_deleted = args.include_deleted;
|
|
10862
|
+
return passthroughResult(await exec({
|
|
10863
|
+
method: "POST",
|
|
10864
|
+
path: `${rowsRoot}/aggregate`,
|
|
10865
|
+
body
|
|
10866
|
+
}));
|
|
10867
|
+
});
|
|
10868
|
+
if (scope === "client" && mapped.hasVector) addTool("find_nearest", {
|
|
10869
|
+
to_text: z.string().optional().describe("Find rows nearest to this text (the server computes the embedding). Exactly one of to_text / to_vector is required."),
|
|
10870
|
+
to_vector: z.array(z.number()).optional().describe("Find rows nearest to this raw vector. Exactly one of to_text / to_vector."),
|
|
10871
|
+
column: z.string().optional().describe(`Vector column to search (default "embedding"). This collection has: ${mapped.vectorColumns.join(", ")}.`),
|
|
10872
|
+
k: z.number().int().positive().optional().describe("How many rows to return (default 10, max 100)."),
|
|
10873
|
+
distance: z.enum([
|
|
10874
|
+
"cosine",
|
|
10875
|
+
"l2",
|
|
10876
|
+
"inner_product"
|
|
10877
|
+
]).optional().describe("Distance metric — must match how the vector index was created (default cosine)."),
|
|
10878
|
+
where: z.object({ ...mapped.whereShape }).partial().optional().describe("Typed, per-column filter applied on top of the nearest search."),
|
|
10879
|
+
include_deleted: z.boolean().optional().describe("Include soft-deleted rows. Default false.")
|
|
10880
|
+
}, async (args, exec) => {
|
|
10881
|
+
const body = {};
|
|
10882
|
+
if (args.to_text !== void 0) body.to_text = args.to_text;
|
|
10883
|
+
if (args.to_vector !== void 0) body.to_vector = args.to_vector;
|
|
10884
|
+
if (args.column !== void 0) body.column = args.column;
|
|
10885
|
+
if (args.k !== void 0) body.k = args.k;
|
|
10886
|
+
if (args.distance !== void 0) body.distance = args.distance;
|
|
10887
|
+
if (args.where !== void 0) body.where = args.where;
|
|
10888
|
+
if (args.include_deleted !== void 0) body.includeDeleted = args.include_deleted;
|
|
10889
|
+
return passthroughResult(await exec({
|
|
10890
|
+
method: "POST",
|
|
10891
|
+
path: `${rowsRoot}/find-nearest`,
|
|
10892
|
+
body
|
|
10893
|
+
}));
|
|
10894
|
+
});
|
|
10895
|
+
}
|
|
10896
|
+
//#endregion
|
|
10897
|
+
//#region src/auto/json-schema-to-zod.ts
|
|
10898
|
+
/**
|
|
10899
|
+
* Declared-function-input-schema → zod shape mapper (auto-MCP Phase 2,
|
|
10900
|
+
* slices C/I).
|
|
10901
|
+
*
|
|
10902
|
+
* A deployed function may DECLARE its input schema in the deploy
|
|
10903
|
+
* annotation (`mcp.input_schema`). The API validates the annotation down
|
|
10904
|
+
* to a SUPPORTED SUBSET of JSON Schema at deploy time (see
|
|
10905
|
+
* `apps/api/src/lib/function-mcp-schema.ts`); this module converts that
|
|
10906
|
+
* subset into the `ZodRawShape` the MCP tool registration consumes, so a
|
|
10907
|
+
* declared schema becomes a genuinely typed tool — the agent sees the
|
|
10908
|
+
* function's real parameters, not a `body: object` blob.
|
|
10909
|
+
*
|
|
10910
|
+
* Supported subset (deliberately mirrored with the deploy-time
|
|
10911
|
+
* validator — change both together):
|
|
10912
|
+
*
|
|
10913
|
+
* - top level: { type: "object", properties, required? }
|
|
10914
|
+
* - property types: string | number | integer | boolean | object | array
|
|
10915
|
+
* - per property: description?, enum? (strings/numbers), items?
|
|
10916
|
+
* - nesting: objects/arrays up to the validator's depth cap
|
|
10917
|
+
*
|
|
10918
|
+
* Fail-safe posture: this runs against schemas the API already validated,
|
|
10919
|
+
* but a manifest could carry an annotation written by an older/newer API.
|
|
10920
|
+
* Any construct we can't map cleanly degrades to `z.unknown()` for that
|
|
10921
|
+
* property (the tool still works; the typing is just looser) — never a
|
|
10922
|
+
* throw, because a registration-time throw would take down the whole
|
|
10923
|
+
* tool surface for one bad annotation.
|
|
10924
|
+
*/
|
|
10925
|
+
function isPlainObject(value) {
|
|
10926
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10927
|
+
}
|
|
10928
|
+
/** Convert one property schema to a zod type. Never throws. */
|
|
10929
|
+
function jsonSchemaPropertyToZod(schema) {
|
|
10930
|
+
if (!isPlainObject(schema)) return z.unknown();
|
|
10931
|
+
const en = schema["enum"];
|
|
10932
|
+
if (Array.isArray(en) && en.length > 0) {
|
|
10933
|
+
if (en.every((v) => typeof v === "string")) {
|
|
10934
|
+
const literals = en.map((v) => z.literal(v));
|
|
10935
|
+
const first = literals[0];
|
|
10936
|
+
if (literals.length === 1) return withDescription(first, schema);
|
|
10937
|
+
return withDescription(z.union([
|
|
10938
|
+
first,
|
|
10939
|
+
literals[1],
|
|
10940
|
+
...literals.slice(2)
|
|
10941
|
+
]), schema);
|
|
10942
|
+
}
|
|
10943
|
+
if (en.every((v) => typeof v === "number")) {
|
|
10944
|
+
const literals = en.map((v) => z.literal(v));
|
|
10945
|
+
const first = literals[0];
|
|
10946
|
+
if (literals.length === 1) return withDescription(first, schema);
|
|
10947
|
+
return withDescription(z.union([
|
|
10948
|
+
first,
|
|
10949
|
+
literals[1],
|
|
10950
|
+
...literals.slice(2)
|
|
10951
|
+
]), schema);
|
|
10952
|
+
}
|
|
10953
|
+
return withDescription(z.unknown(), schema);
|
|
10954
|
+
}
|
|
10955
|
+
switch (schema["type"]) {
|
|
10956
|
+
case "string": return withDescription(z.string(), schema);
|
|
10957
|
+
case "number": return withDescription(z.number(), schema);
|
|
10958
|
+
case "integer": return withDescription(z.number().int(), schema);
|
|
10959
|
+
case "boolean": return withDescription(z.boolean(), schema);
|
|
10960
|
+
case "array": {
|
|
10961
|
+
const items = schema["items"];
|
|
10962
|
+
const elem = items === void 0 ? z.unknown() : jsonSchemaPropertyToZod(items);
|
|
10963
|
+
return withDescription(z.array(elem), schema);
|
|
10964
|
+
}
|
|
10965
|
+
case "object": {
|
|
10966
|
+
const props = schema["properties"];
|
|
10967
|
+
if (!isPlainObject(props)) return withDescription(z.record(z.unknown()), schema);
|
|
10968
|
+
const required = new Set(Array.isArray(schema["required"]) ? schema["required"].filter((r) => typeof r === "string") : []);
|
|
10969
|
+
const shape = {};
|
|
10970
|
+
for (const [key, value] of Object.entries(props)) {
|
|
10971
|
+
const mapped = jsonSchemaPropertyToZod(value);
|
|
10972
|
+
shape[key] = required.has(key) ? mapped : mapped.optional();
|
|
10973
|
+
}
|
|
10974
|
+
return withDescription(z.object(shape), schema);
|
|
10975
|
+
}
|
|
10976
|
+
default: return withDescription(z.unknown(), schema);
|
|
10977
|
+
}
|
|
10978
|
+
}
|
|
10979
|
+
function withDescription(type, schema) {
|
|
10980
|
+
const description = schema["description"];
|
|
10981
|
+
if (typeof description === "string" && description.length > 0) return type.describe(description);
|
|
10982
|
+
return type;
|
|
10983
|
+
}
|
|
10984
|
+
/**
|
|
10985
|
+
* Convert a declared top-level input schema to a `ZodRawShape` (the MCP
|
|
10986
|
+
* tool registration input). Returns `null` when the schema is not the
|
|
10987
|
+
* supported `type: "object"` shape — the caller falls back to the
|
|
10988
|
+
* generic `{ body?, query? }` tool input.
|
|
10989
|
+
*/
|
|
10990
|
+
function jsonSchemaToZodShape(schema) {
|
|
10991
|
+
if (!isPlainObject(schema) || schema["type"] !== "object") return null;
|
|
10992
|
+
const props = schema["properties"];
|
|
10993
|
+
if (!isPlainObject(props)) return null;
|
|
10994
|
+
const required = new Set(Array.isArray(schema["required"]) ? schema["required"].filter((r) => typeof r === "string") : []);
|
|
10995
|
+
const shape = {};
|
|
10996
|
+
for (const [key, value] of Object.entries(props)) {
|
|
10997
|
+
const mapped = jsonSchemaPropertyToZod(value);
|
|
10998
|
+
shape[key] = required.has(key) ? mapped : mapped.optional();
|
|
10999
|
+
}
|
|
11000
|
+
return shape;
|
|
11001
|
+
}
|
|
11002
|
+
//#endregion
|
|
11003
|
+
//#region src/auto/function-tools.ts
|
|
11004
|
+
/** Build the `<ns>_fn_<name>` tool name. */
|
|
11005
|
+
function functionToolName(namespace, fn) {
|
|
11006
|
+
return `${sanitizeNameFragment(namespace)}_fn_${sanitizeNameFragment(fn)}`;
|
|
11007
|
+
}
|
|
11008
|
+
/** Generic input shape for functions without a declared schema. */
|
|
11009
|
+
const GENERIC_FUNCTION_SHAPE = {
|
|
11010
|
+
body: z.unknown().optional().describe("JSON request body forwarded to the function as-is."),
|
|
11011
|
+
query: z.record(z.string()).optional().describe("String key/value pairs appended to the invocation URL as query parameters.")
|
|
11012
|
+
};
|
|
11013
|
+
/**
|
|
11014
|
+
* Invoke the function through its public execution URL with the
|
|
11015
|
+
* connection's credential. Returns `{ status, body }` — the function's
|
|
11016
|
+
* response verbatim (JSON when parseable, text otherwise).
|
|
11017
|
+
*/
|
|
11018
|
+
async function invokeFunction(input) {
|
|
11019
|
+
let url = input.invokeUrl;
|
|
11020
|
+
if (input.query && Object.keys(input.query).length > 0) {
|
|
11021
|
+
const qs = new URLSearchParams(input.query).toString();
|
|
11022
|
+
url += url.includes("?") ? `&${qs}` : `?${qs}`;
|
|
11023
|
+
}
|
|
11024
|
+
const headers = {
|
|
11025
|
+
Accept: "application/json",
|
|
11026
|
+
"X-Api-Key": input.clientAuth.apiKey
|
|
11027
|
+
};
|
|
11028
|
+
if (input.clientAuth.sessionToken) headers["Authorization"] = `Bearer ${input.clientAuth.sessionToken}`;
|
|
11029
|
+
let bodyText;
|
|
11030
|
+
if (input.body !== void 0) {
|
|
11031
|
+
headers["Content-Type"] = "application/json";
|
|
11032
|
+
bodyText = JSON.stringify(input.body);
|
|
11033
|
+
}
|
|
11034
|
+
const res = await fetch(url, {
|
|
11035
|
+
method: "POST",
|
|
11036
|
+
headers,
|
|
11037
|
+
body: bodyText
|
|
11038
|
+
});
|
|
11039
|
+
const text = await res.text();
|
|
11040
|
+
let parsed = text;
|
|
11041
|
+
try {
|
|
11042
|
+
parsed = text.length > 0 ? JSON.parse(text) : null;
|
|
11043
|
+
} catch {}
|
|
11044
|
+
return {
|
|
11045
|
+
status: res.status,
|
|
11046
|
+
body: parsed
|
|
11047
|
+
};
|
|
11048
|
+
}
|
|
11049
|
+
/**
|
|
11050
|
+
* Register the tool for a single exposed function on `server`.
|
|
11051
|
+
*/
|
|
11052
|
+
function registerFunctionToolsFor(server, options) {
|
|
11053
|
+
const { namespace, fn, clientAuth } = options;
|
|
11054
|
+
const toolName = functionToolName(namespace, fn.name);
|
|
11055
|
+
const declaredShape = fn.input_schema ? jsonSchemaToZodShape(fn.input_schema) : null;
|
|
11056
|
+
const description = describeFunctionTool({
|
|
11057
|
+
fn: fn.name,
|
|
11058
|
+
declaredDescription: fn.description ?? null,
|
|
11059
|
+
hasDeclaredSchema: declaredShape !== null,
|
|
11060
|
+
isPublic: fn.public === true
|
|
11061
|
+
});
|
|
11062
|
+
if (declaredShape) {
|
|
11063
|
+
registerPublicTool(server, toolName, description, declaredShape, async (args) => {
|
|
11064
|
+
return jsonResult$1(await invokeFunction({
|
|
11065
|
+
invokeUrl: fn.invoke_url,
|
|
11066
|
+
clientAuth,
|
|
11067
|
+
body: args
|
|
11068
|
+
}));
|
|
11069
|
+
});
|
|
11070
|
+
return;
|
|
11071
|
+
}
|
|
11072
|
+
registerPublicTool(server, toolName, description, GENERIC_FUNCTION_SHAPE, async (args) => {
|
|
11073
|
+
const { body, query } = args;
|
|
11074
|
+
return jsonResult$1(await invokeFunction({
|
|
11075
|
+
invokeUrl: fn.invoke_url,
|
|
11076
|
+
clientAuth,
|
|
11077
|
+
body,
|
|
11078
|
+
...query ? { query } : {}
|
|
11079
|
+
}));
|
|
11080
|
+
});
|
|
11081
|
+
}
|
|
11082
|
+
//#endregion
|
|
11083
|
+
//#region src/auto/index.ts
|
|
11084
|
+
/** Verbs a collection's typed tool set advertises (per scope + schema). */
|
|
11085
|
+
function verbsFor(scope, schema) {
|
|
11086
|
+
const verbs = [
|
|
11087
|
+
"list",
|
|
11088
|
+
"get",
|
|
11089
|
+
"find",
|
|
11090
|
+
"insert",
|
|
11091
|
+
"update",
|
|
11092
|
+
"delete",
|
|
11093
|
+
"aggregate"
|
|
11094
|
+
];
|
|
11095
|
+
if (scope === "client" && mapCollectionSchema(schema).hasVector) verbs.push("find_nearest");
|
|
11096
|
+
return verbs;
|
|
11097
|
+
}
|
|
11098
|
+
/** Admin GET helper bound to a Bearer (no pat-arg machinery needed here). */
|
|
11099
|
+
async function adminGet(apiClient, path) {
|
|
11100
|
+
const res = await fetch(`${apiClient.getBaseUrl()}${path}`, {
|
|
11101
|
+
method: "GET",
|
|
11102
|
+
headers: {
|
|
11103
|
+
Authorization: `Bearer ${await apiClientToken(apiClient)}`,
|
|
11104
|
+
Accept: "application/json"
|
|
11105
|
+
}
|
|
11106
|
+
});
|
|
11107
|
+
let body;
|
|
11108
|
+
try {
|
|
11109
|
+
body = await res.json();
|
|
11110
|
+
} catch {
|
|
11111
|
+
body = {};
|
|
11112
|
+
}
|
|
11113
|
+
return {
|
|
11114
|
+
status: res.status,
|
|
11115
|
+
body
|
|
11116
|
+
};
|
|
11117
|
+
}
|
|
11118
|
+
async function apiClientToken(apiClient) {
|
|
11119
|
+
return await apiClient.resolveTokenOrNull() ?? "";
|
|
11120
|
+
}
|
|
11121
|
+
/**
|
|
11122
|
+
* Register the auto-MCP tool surface for one project on `server`.
|
|
11123
|
+
*
|
|
11124
|
+
* Returns the names of the collections that were exposed plus `listOk`
|
|
11125
|
+
* (false when no manifest was available — in which case NO data tools
|
|
11126
|
+
* register and the meta tools carry the diagnostic).
|
|
11127
|
+
*/
|
|
11128
|
+
async function registerAutoMcpTools(server, apiClient, options) {
|
|
11129
|
+
const { projectId, scope, manifest, clientAuth } = options;
|
|
11130
|
+
const namespace = sanitizeNameFragment(options.namespace ?? (manifest?.slug ? manifest.slug : projectId));
|
|
11131
|
+
if (scope === "client") registerPublicTool(server, `${namespace}_whoami`, describeWhoami(namespace), {}, async () => jsonResult$1({
|
|
11132
|
+
status: 200,
|
|
11133
|
+
project_id: projectId,
|
|
11134
|
+
scope,
|
|
11135
|
+
namespace,
|
|
11136
|
+
authenticated: Boolean(clientAuth),
|
|
11137
|
+
end_user_session: Boolean(clientAuth?.sessionToken)
|
|
11138
|
+
}));
|
|
11139
|
+
else registerTool(server, apiClient, `${namespace}_whoami`, describeWhoami(namespace), {}, async (_args, { pat }) => {
|
|
11140
|
+
const me = await adminGet(apiClient.withToken(pat), `/projects/${projectId}`);
|
|
11141
|
+
return jsonResult$1({
|
|
11142
|
+
status: me.status,
|
|
11143
|
+
project_id: projectId,
|
|
11144
|
+
scope,
|
|
11145
|
+
namespace,
|
|
11146
|
+
authenticated: me.status >= 200 && me.status < 300
|
|
11147
|
+
});
|
|
11148
|
+
});
|
|
11149
|
+
if (!manifest) {
|
|
11150
|
+
const diagnosticHandler = async () => jsonResult$1({
|
|
11151
|
+
status: 503,
|
|
11152
|
+
list_complete: false,
|
|
11153
|
+
namespace,
|
|
11154
|
+
project_id: projectId,
|
|
11155
|
+
scope,
|
|
11156
|
+
error: {
|
|
11157
|
+
code: "MANIFEST_UNAVAILABLE",
|
|
11158
|
+
message: "The app tool manifest could not be loaded for this connection. Retry shortly; if it persists, verify the credential belongs to this app and that its agent surface is enabled."
|
|
11159
|
+
},
|
|
11160
|
+
collections: [],
|
|
11161
|
+
functions: []
|
|
11162
|
+
});
|
|
11163
|
+
if (scope === "client") registerPublicTool(server, `${namespace}_list_collections`, describeListCollections(namespace), {}, diagnosticHandler);
|
|
11164
|
+
else registerTool(server, apiClient, `${namespace}_list_collections`, describeListCollections(namespace), {}, diagnosticHandler);
|
|
11165
|
+
return {
|
|
11166
|
+
namespace,
|
|
11167
|
+
collections: [],
|
|
11168
|
+
listOk: false
|
|
11169
|
+
};
|
|
11170
|
+
}
|
|
11171
|
+
const manifestFunctions = (manifest.functions ?? []).filter((f) => f && typeof f.name === "string" && f.name.length > 0 && typeof f.invoke_url === "string");
|
|
11172
|
+
const exposedCollections = manifest.collections.map((c) => ({
|
|
11173
|
+
collection: c.name,
|
|
11174
|
+
exposed: true,
|
|
11175
|
+
tools: verbsFor(scope, c).map((v) => collectionToolName(namespace, c.name, v))
|
|
11176
|
+
}));
|
|
11177
|
+
const exposedFunctions = manifestFunctions.map((f) => ({
|
|
11178
|
+
function: f.name,
|
|
11179
|
+
exposed: scope === "client",
|
|
11180
|
+
tools: scope === "client" ? [functionToolName(namespace, f.name)] : [],
|
|
11181
|
+
typed: Boolean(f.input_schema)
|
|
11182
|
+
}));
|
|
11183
|
+
const listCollectionsHandler = async () => jsonResult$1({
|
|
11184
|
+
status: 200,
|
|
11185
|
+
list_complete: true,
|
|
11186
|
+
namespace,
|
|
11187
|
+
project_id: projectId,
|
|
11188
|
+
scope,
|
|
11189
|
+
manifest_version: manifest.version,
|
|
11190
|
+
exposure_note: exposedCollections.length === 0 && exposedFunctions.length === 0 ? "Nothing is exposed on this agent surface yet. Exposure is an explicit allowlist the app developer manages (collections and functions are NOT exposed by default)." : void 0,
|
|
11191
|
+
collections: exposedCollections,
|
|
11192
|
+
functions: exposedFunctions
|
|
11193
|
+
});
|
|
11194
|
+
if (scope === "client") registerPublicTool(server, `${namespace}_list_collections`, describeListCollections(namespace), {}, listCollectionsHandler);
|
|
11195
|
+
else registerTool(server, apiClient, `${namespace}_list_collections`, describeListCollections(namespace), {}, listCollectionsHandler);
|
|
11196
|
+
for (const entry of manifest.collections) {
|
|
11197
|
+
const policy = {
|
|
11198
|
+
read_policy: entry.read_policy ?? "owner",
|
|
11199
|
+
write_policy: entry.write_policy ?? "owner"
|
|
11200
|
+
};
|
|
11201
|
+
registerCollectionToolsFor(server, apiClient, {
|
|
11202
|
+
projectId,
|
|
11203
|
+
namespace,
|
|
11204
|
+
collection: entry.name,
|
|
11205
|
+
schema: {
|
|
11206
|
+
name: entry.name,
|
|
11207
|
+
columns: entry.columns
|
|
11208
|
+
},
|
|
11209
|
+
policy,
|
|
11210
|
+
scope,
|
|
11211
|
+
clientAuth
|
|
11212
|
+
});
|
|
11213
|
+
}
|
|
11214
|
+
if (scope === "client" && clientAuth) for (const fn of manifestFunctions) registerFunctionToolsFor(server, {
|
|
11215
|
+
namespace,
|
|
11216
|
+
fn,
|
|
11217
|
+
clientAuth: {
|
|
11218
|
+
apiKey: clientAuth.apiKey,
|
|
11219
|
+
...clientAuth.sessionToken ? { sessionToken: clientAuth.sessionToken } : {}
|
|
11220
|
+
}
|
|
11221
|
+
});
|
|
11222
|
+
return {
|
|
11223
|
+
namespace,
|
|
11224
|
+
collections: manifest.collections.map((c) => c.name),
|
|
11225
|
+
listOk: true
|
|
11226
|
+
};
|
|
11227
|
+
}
|
|
11228
|
+
//#endregion
|
|
8667
11229
|
//#region src/index.ts
|
|
8668
11230
|
/**
|
|
8669
11231
|
* Register every Amba MCP tool group against the given server, using the
|
|
@@ -8671,7 +11233,16 @@ function getToolCategory(toolName) {
|
|
|
8671
11233
|
* called against it once.
|
|
8672
11234
|
*/
|
|
8673
11235
|
function registerAllTools(server, apiClient) {
|
|
8674
|
-
registerTools$
|
|
11236
|
+
registerTools$20(server, apiClient);
|
|
11237
|
+
registerTools$42(server, apiClient);
|
|
11238
|
+
registerTools$5(server, apiClient);
|
|
11239
|
+
registerTools$3(server, apiClient);
|
|
11240
|
+
registerTools$41(server, apiClient);
|
|
11241
|
+
registerTools$40(server, apiClient);
|
|
11242
|
+
registerTools$39(server, apiClient);
|
|
11243
|
+
registerTools$38(server, apiClient);
|
|
11244
|
+
registerTools$37(server, apiClient);
|
|
11245
|
+
registerTools$36(server, apiClient);
|
|
8675
11246
|
registerTools$35(server, apiClient);
|
|
8676
11247
|
registerTools$34(server, apiClient);
|
|
8677
11248
|
registerTools$33(server, apiClient);
|
|
@@ -8687,12 +11258,12 @@ function registerAllTools(server, apiClient) {
|
|
|
8687
11258
|
registerTools$23(server, apiClient);
|
|
8688
11259
|
registerTools$22(server, apiClient);
|
|
8689
11260
|
registerTools$21(server, apiClient);
|
|
8690
|
-
registerTools$20(server, apiClient);
|
|
8691
11261
|
registerTools$19(server, apiClient);
|
|
8692
11262
|
registerTools$18(server, apiClient);
|
|
8693
11263
|
registerTools$17(server, apiClient);
|
|
8694
11264
|
registerTools$16(server, apiClient);
|
|
8695
11265
|
registerTools$15(server, apiClient);
|
|
11266
|
+
registerTools$14(server, apiClient);
|
|
8696
11267
|
registerTools$13(server, apiClient);
|
|
8697
11268
|
registerTools$12(server, apiClient);
|
|
8698
11269
|
registerTools$11(server, apiClient);
|
|
@@ -8701,9 +11272,7 @@ function registerAllTools(server, apiClient) {
|
|
|
8701
11272
|
registerTools$8(server, apiClient);
|
|
8702
11273
|
registerTools$7(server, apiClient);
|
|
8703
11274
|
registerTools$6(server, apiClient);
|
|
8704
|
-
registerTools$5(server, apiClient);
|
|
8705
11275
|
registerTools$4(server, apiClient);
|
|
8706
|
-
registerTools$3(server, apiClient);
|
|
8707
11276
|
registerTools$2(server, apiClient);
|
|
8708
11277
|
registerTools$1(server, apiClient);
|
|
8709
11278
|
registerTools(server, apiClient);
|
|
@@ -8727,4 +11296,4 @@ function createApiClient(options) {
|
|
|
8727
11296
|
return new ApiClient(opts);
|
|
8728
11297
|
}
|
|
8729
11298
|
//#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 };
|
|
11299
|
+
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, describeFunctionTool, describeListCollections, describeWhoami, functionToolName, getToolCategory, jsonSchemaToZodShape, mapCollectionSchema, registerAllResources, registerAllTools, registerAutoMcpTools, registerCollectionToolsFor, registerFunctionToolsFor, sanitizeNameFragment };
|