@layers/amba-mcp 4.0.11 → 4.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -48,20 +48,50 @@ function isTokenExpired(credentials) {
48
48
  var AmbaApiError = class extends Error {
49
49
  status;
50
50
  code;
51
- constructor(status, code, message) {
51
+ /** The API's own `error.message`, without the `API error <status>:` prefix. */
52
+ apiMessage;
53
+ /**
54
+ * The API's `error.details`, when the response carried one: for example a
55
+ * 402 `FREE_TIER_LIMIT_REACHED` names the limit, the reset date, the
56
+ * billing URL and the one-call upgrade there. Tool results pass it to the
57
+ * agent (`lib/tool-result.ts` `apiErrorResult`).
58
+ */
59
+ details;
60
+ constructor(status, code, message, details) {
52
61
  super(`API error ${status}: ${message}`);
53
62
  this.name = "AmbaApiError";
54
63
  this.status = status;
55
64
  this.code = code;
65
+ this.apiMessage = message;
66
+ this.details = details;
56
67
  }
57
68
  };
69
+ /**
70
+ * Read a non-2xx response's `{ error: { code, message, details } }` envelope
71
+ * into an `AmbaApiError`. A body that is not that JSON envelope falls back to
72
+ * the status text.
73
+ */
74
+ async function ambaApiErrorFromResponse(res) {
75
+ let errorMessage = res.statusText;
76
+ let errorCode;
77
+ let details;
78
+ try {
79
+ const err = (await res.json())?.error;
80
+ if (typeof err?.message === "string") errorMessage = err.message;
81
+ if (typeof err?.code === "string") errorCode = err.code;
82
+ details = err?.details;
83
+ } catch {}
84
+ return new AmbaApiError(res.status, errorCode, errorMessage, details);
85
+ }
58
86
  const BASE_URL = "https://api.amba.dev/v1/admin";
59
87
  var ApiClient = class ApiClient {
88
+ authProxyHeaders;
60
89
  baseUrl;
61
90
  apiRoot;
62
91
  tokenProvider;
63
92
  signupToken;
64
93
  constructor(options) {
94
+ this.authProxyHeaders = { ...options?.authProxyHeaders };
65
95
  const raw = (options?.baseUrl ?? BASE_URL).replace(/\/+$/, "");
66
96
  if (!/\/admin$/.test(raw)) throw new Error(`ApiClient baseUrl must end in /admin (got ${JSON.stringify(raw)}). Set AMBA_API_URL=https://api.amba.dev/v1/admin (or your equivalent versioned host).`);
67
97
  this.baseUrl = raw;
@@ -114,6 +144,15 @@ var ApiClient = class ApiClient {
114
144
  return this.apiRoot;
115
145
  }
116
146
  /**
147
+ * Headers to add to public developer-auth calls (see
148
+ * `ApiClientOptions.authProxyHeaders`). Those calls always run on the
149
+ * client the tools were registered with, so a `withToken` sibling does not
150
+ * need to carry them.
151
+ */
152
+ getAuthProxyHeaders() {
153
+ return { ...this.authProxyHeaders };
154
+ }
155
+ /**
117
156
  * Resolve the current developer Bearer token, going through the configured
118
157
  * tokenProvider (or `~/.amba/credentials.json` for the CLI fallback).
119
158
  *
@@ -146,6 +185,23 @@ var ApiClient = class ApiClient {
146
185
  if (isTokenExpired(credentials)) throw new Error("Developer access token has expired. Run \"amba login\" to re-authenticate.");
147
186
  return credentials.access_token;
148
187
  }
188
+ /**
189
+ * Account notices (`X-Amba-Notice`, e.g. the unclaimed-sandbox claim
190
+ * instruction) seen on responses since the last `takeNotices()`. The
191
+ * `registerTool` wrapper drains them into the tool result so the agent
192
+ * reads them alongside the data it asked for.
193
+ */
194
+ notices = /* @__PURE__ */ new Set();
195
+ /** Return and clear the notices collected since the last call. */
196
+ takeNotices() {
197
+ const out = [...this.notices];
198
+ this.notices.clear();
199
+ return out;
200
+ }
201
+ captureNotice(res) {
202
+ const notice = res.headers?.get?.("x-amba-notice");
203
+ if (notice) this.notices.add(notice);
204
+ }
149
205
  async request(method, path, body, query) {
150
206
  const token = await this.getToken();
151
207
  let url = `${this.baseUrl}${path}`;
@@ -163,16 +219,8 @@ var ApiClient = class ApiClient {
163
219
  headers,
164
220
  body: body !== void 0 ? JSON.stringify(body) : void 0
165
221
  });
166
- if (!res.ok) {
167
- let errorMessage = res.statusText;
168
- let errorCode;
169
- try {
170
- const errorBody = await res.json();
171
- errorMessage = errorBody?.error?.message ?? res.statusText;
172
- errorCode = errorBody?.error?.code;
173
- } catch {}
174
- throw new AmbaApiError(res.status, errorCode, errorMessage);
175
- }
222
+ this.captureNotice(res);
223
+ if (!res.ok) throw await ambaApiErrorFromResponse(res);
176
224
  if (res.status === 204) return;
177
225
  return await res.json();
178
226
  }
@@ -215,16 +263,8 @@ var ApiClient = class ApiClient {
215
263
  Accept: "text/csv, application/x-ndjson, */*"
216
264
  }
217
265
  });
218
- if (!res.ok) {
219
- let errorMessage = res.statusText;
220
- let errorCode;
221
- try {
222
- const errorBody = await res.json();
223
- errorMessage = errorBody?.error?.message ?? res.statusText;
224
- errorCode = errorBody?.error?.code;
225
- } catch {}
226
- throw new AmbaApiError(res.status, errorCode, errorMessage);
227
- }
266
+ this.captureNotice(res);
267
+ if (!res.ok) throw await ambaApiErrorFromResponse(res);
228
268
  const contentType = res.headers.get("content-type");
229
269
  const reader = res.body?.getReader();
230
270
  if (!reader) {
@@ -387,6 +427,7 @@ const WRITE_VERBS = new Set([
387
427
  "insert",
388
428
  "invite",
389
429
  "register",
430
+ "claim",
390
431
  "define",
391
432
  "map",
392
433
  "adopt",
@@ -403,7 +444,8 @@ const WRITE_VERBS = new Set([
403
444
  "generate",
404
445
  "enroll",
405
446
  "request",
406
- "clawback"
447
+ "clawback",
448
+ "upgrade"
407
449
  ]);
408
450
  /**
409
451
  * Split a tool name into lowercase verb-candidate tokens, dropping the
@@ -562,8 +604,174 @@ function deriveToolAnnotations(name) {
562
604
  }
563
605
  }
564
606
  //#endregion
607
+ //#region src/lib/tool-result.ts
608
+ /**
609
+ * Shared MCP tool result helpers.
610
+ *
611
+ * Every tool emits the same `{ content: [{ type: 'text', text: <json> }] }`
612
+ * envelope. Two helpers centralize that:
613
+ *
614
+ * - [`jsonResult`] — wraps an arbitrary payload.
615
+ * - [`passthroughResult`] — flattens an upstream HTTP response
616
+ * (status + parsed body) into the same envelope. Status is written
617
+ * LAST so a colliding top-level `status` field in the API response
618
+ * cannot shadow the HTTP status — agents look at `parsed.status` to
619
+ * distinguish 2xx from 4xx/5xx.
620
+ *
621
+ * Lives in `src/lib/` (vs. `src/tools/_helpers.ts`) to set the same
622
+ * cross-cutting-helper precedent as `src/lib/with-pat.ts` (task #36).
623
+ * `tools/*` files stay strictly tool registrations.
624
+ */
625
+ /**
626
+ * Wire-shape every MCP tool handler returns.
627
+ *
628
+ * The MCP SDK's `tool()` callback signature is structurally typed and
629
+ * carries an open index signature for `_meta` etc. Declaring our return
630
+ * type as a plain `{ content: [...] }` interface won't satisfy that
631
+ * structural check — so the helpers' return type is left as the actual
632
+ * inferred shape (no explicit interface) and consumers rely on the
633
+ * inference + the SDK's structural compatibility. If we ever want a
634
+ * named alias, write it as a type-alias over the inferred shape rather
635
+ * than a closed interface.
636
+ */
637
+ /** Wrap an arbitrary payload as a JSON-text tool result. */
638
+ function jsonResult$1(payload) {
639
+ return { content: [{
640
+ type: "text",
641
+ text: JSON.stringify(payload, null, 2)
642
+ }] };
643
+ }
644
+ /** Preserve an HTTP response wrapper while mapping failures to MCP isError. */
645
+ function httpResult(result) {
646
+ return {
647
+ ...jsonResult$1(result),
648
+ ...result.status >= 400 ? { isError: true } : {}
649
+ };
650
+ }
651
+ /**
652
+ * Flatten an upstream HTTP response into the agent-facing tool payload.
653
+ * Body fields are spread FIRST so a future top-level `status` key in
654
+ * the API response cannot shadow the HTTP `status` — agents read
655
+ * `parsed.status` to distinguish 2xx from 4xx/5xx.
656
+ */
657
+ function passthroughResult(result) {
658
+ return {
659
+ ...jsonResult$1({
660
+ ...result.body !== null && typeof result.body === "object" ? result.body : {},
661
+ status: result.status
662
+ }),
663
+ ...result.status >= 400 ? { isError: true } : {}
664
+ };
665
+ }
666
+ /**
667
+ * Tool result for a failed API call. The first text item is the
668
+ * human-readable line tools have always returned (`API error 402: ...`). The
669
+ * second is the API's structured error as JSON,
670
+ * `{ "status": 402, "error": { "code", "message", "details" } }`, so an agent
671
+ * can branch on `error.code` and read `details` (a free-tier 402 carries the
672
+ * limit, the reset date, the billing URL and the one-call upgrade there).
673
+ *
674
+ * Code mode joins an error result's text items with a newline into the
675
+ * exception it throws in the sandbox, so the same JSON reaches that surface
676
+ * after the first line of the error message.
677
+ */
678
+ function apiErrorResult(err) {
679
+ const error = {
680
+ code: err.code ?? null,
681
+ message: err.apiMessage
682
+ };
683
+ if (err.details !== void 0) error.details = err.details;
684
+ return {
685
+ content: [{
686
+ type: "text",
687
+ text: err.message
688
+ }, {
689
+ type: "text",
690
+ text: JSON.stringify({
691
+ status: err.status,
692
+ error
693
+ }, null, 2)
694
+ }],
695
+ isError: true
696
+ };
697
+ }
698
+ //#endregion
565
699
  //#region src/lib/with-pat.ts
566
700
  /**
701
+ * Attach the account notices the API sent with this call's responses
702
+ * (`X-Amba-Notice`, collected by `ApiClient`), so an agent sees "claim this
703
+ * account" next to what it asked for. A successful result gets them as extra
704
+ * text items. An error result gets them as a `notices` array inside its
705
+ * trailing JSON error, so that item stays the last one and stays parseable
706
+ * (code mode joins the text items into the message of the Error it throws).
707
+ * A result without notices is returned unchanged.
708
+ */
709
+ function withAccountNotices(result, client) {
710
+ const take = client.takeNotices;
711
+ const notices = typeof take === "function" ? take.call(client) : [];
712
+ if (notices.length === 0) return result;
713
+ if (result.isError) {
714
+ const folded = foldNoticesIntoErrorJson(result, notices);
715
+ if (folded) return folded;
716
+ }
717
+ return {
718
+ ...result,
719
+ content: [...result.content, ...notices.map((notice) => ({
720
+ type: "text",
721
+ text: `Notice from Amba: ${notice}`
722
+ }))]
723
+ };
724
+ }
725
+ /** `result` with `notices` merged into its last text item, or null when that item is not a JSON object. */
726
+ function foldNoticesIntoErrorJson(result, notices) {
727
+ const last = result.content[result.content.length - 1];
728
+ if (!last) return null;
729
+ let parsed;
730
+ try {
731
+ parsed = JSON.parse(last.text);
732
+ } catch {
733
+ return null;
734
+ }
735
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
736
+ const payload = parsed;
737
+ const existing = Array.isArray(payload["notices"]) ? payload["notices"] : [];
738
+ const merged = {
739
+ ...payload,
740
+ notices: [...existing, ...notices]
741
+ };
742
+ return {
743
+ ...result,
744
+ content: [...result.content.slice(0, -1), {
745
+ type: "text",
746
+ text: JSON.stringify(merged, null, 2)
747
+ }]
748
+ };
749
+ }
750
+ /**
751
+ * True for an `AmbaApiError`. The duck-typed branch also accepts one thrown
752
+ * by a second copy of this package (a bundler that inlines it twice), where
753
+ * `instanceof` against this copy's class is false.
754
+ */
755
+ function isAmbaApiError(err) {
756
+ if (err instanceof AmbaApiError) return true;
757
+ if (!(err instanceof Error) || err.name !== "AmbaApiError") return false;
758
+ const e = err;
759
+ return typeof e.status === "number" && typeof e.apiMessage === "string";
760
+ }
761
+ /**
762
+ * Run a tool handler; a failed API call it throws becomes an error result
763
+ * that keeps the API's `code`, `message` and `details` (`apiErrorResult`).
764
+ * Any other throw propagates, and the MCP SDK reports its message.
765
+ */
766
+ async function structuredApiErrors(run) {
767
+ try {
768
+ return await run();
769
+ } catch (err) {
770
+ if (isAmbaApiError(err)) return apiErrorResult(err);
771
+ throw err;
772
+ }
773
+ }
774
+ /**
567
775
  * The `pat` schema entry injected into every tool that goes through
568
776
  * `registerTool`. Exported so tests can assert on its presence.
569
777
  */
@@ -578,26 +786,30 @@ const PAT_ARG_SCHEMA = z.string().optional().describe([
578
786
  ].join(" "));
579
787
  /**
580
788
  * Build the MISSING_PAT short-circuit payload. Shape matches the auth
581
- * tools so agents can parse `error.code` uniformly.
789
+ * tools so agents can parse `error.code` uniformly. It is an error result
790
+ * like any other failed call (code mode throws it).
582
791
  */
583
792
  function missingPatResult(toolName) {
584
- return { content: [{
585
- type: "text",
586
- text: JSON.stringify({
587
- status: 401,
588
- error: {
589
- code: "MISSING_PAT",
590
- message: [
591
- `Tool "${toolName}" requires a developer Bearer.`,
592
- "Pass `pat` as a tool argument (the value returned by",
593
- "`amba_developer_signup` / `amba_developer_login`). The next",
594
- "agent session will pick the PAT up automatically once the",
595
- "snippet in `mcp_config` is written to the customer's MCP",
596
- "client config, at which point the `pat` arg becomes optional."
597
- ].join(" ")
598
- }
599
- }, null, 2)
600
- }] };
793
+ return {
794
+ isError: true,
795
+ content: [{
796
+ type: "text",
797
+ text: JSON.stringify({
798
+ status: 401,
799
+ error: {
800
+ code: "MISSING_PAT",
801
+ message: [
802
+ `Tool "${toolName}" requires a developer Bearer.`,
803
+ "Pass `pat` as a tool argument (the value returned by",
804
+ "`amba_developer_signup` / `amba_developer_login`). The next",
805
+ "agent session will pick the PAT up automatically once the",
806
+ "snippet in `mcp_config` is written to the customer's MCP",
807
+ "client config, at which point the `pat` arg becomes optional."
808
+ ].join(" ")
809
+ }
810
+ }, null, 2)
811
+ }]
812
+ };
601
813
  }
602
814
  /**
603
815
  * Register a tool that:
@@ -633,10 +845,10 @@ function registerTool(server, apiClient, name, description, schema, handler, ali
633
845
  client = apiClient;
634
846
  }
635
847
  if (!pat) return missingPatResult(registeredName);
636
- return handler(rawArgs, {
848
+ return withAccountNotices(await structuredApiErrors(() => handler(rawArgs, {
637
849
  pat,
638
850
  client
639
- });
851
+ })), client);
640
852
  };
641
853
  server.tool(name, description, extendedSchema, deriveToolAnnotations(name), wrappedHandler(name));
642
854
  for (const alias of aliases) server.tool(alias, description, extendedSchema, deriveToolAnnotations(alias), (async (rawArgs) => {
@@ -651,15 +863,16 @@ function registerTool(server, apiClient, name, description, schema, handler, ali
651
863
  * should go through `registerTool`.
652
864
  */
653
865
  function registerPublicTool(server, name, description, schema, handler, aliases = [], annotations) {
654
- server.tool(name, description, schema, annotations ?? deriveToolAnnotations(name), handler);
866
+ const wrappedHandler = (args) => structuredApiErrors(() => handler(args));
867
+ server.tool(name, description, schema, annotations ?? deriveToolAnnotations(name), wrappedHandler);
655
868
  for (const alias of aliases) server.tool(alias, description, schema, annotations ?? deriveToolAnnotations(alias), (async (args) => {
656
869
  warnDeprecatedAlias(alias, name);
657
- return handler(args);
870
+ return wrappedHandler(args);
658
871
  }));
659
872
  }
660
873
  //#endregion
661
874
  //#region src/tools/projects.ts
662
- function registerTools$42(server, apiClient) {
875
+ function registerTools$43(server, apiClient) {
663
876
  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 }) => {
664
877
  const result = await client.get("/projects");
665
878
  return { content: [{
@@ -770,7 +983,7 @@ function registerTools$42(server, apiClient) {
770
983
  }
771
984
  //#endregion
772
985
  //#region src/tools/push.ts
773
- function registerTools$41(server, apiClient) {
986
+ function registerTools$42(server, apiClient) {
774
987
  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.", {
775
988
  project_id: z.string().describe("The project ID"),
776
989
  title: z.string().describe("Push notification title shown to the user"),
@@ -900,7 +1113,7 @@ const segmentRulesSchema = z.object({
900
1113
  operator: z.enum(["AND", "OR"]).describe("Logical operator combining conditions"),
901
1114
  conditions: z.array(segmentConditionSchema).describe("Array of filter conditions")
902
1115
  });
903
- function registerTools$40(server, apiClient) {
1116
+ function registerTools$41(server, apiClient) {
904
1117
  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\").", {
905
1118
  project_id: z.string().describe("The project ID"),
906
1119
  include_system: z.boolean().optional().describe("Include built-in system segments in the result. Defaults to false.")
@@ -985,7 +1198,7 @@ const configConditionSchema = z.object({
985
1198
  percentage: z.number().optional().describe("Percentage rollout (0-100)"),
986
1199
  value: z.unknown().describe("Override value for this condition")
987
1200
  });
988
- function registerTools$39(server, apiClient) {
1201
+ function registerTools$40(server, apiClient) {
989
1202
  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).", {
990
1203
  project_id: z.string().describe("The project ID"),
991
1204
  include_system: z.boolean().optional().describe("Include platform-provided default config keys in the result. Defaults to false.")
@@ -1063,7 +1276,7 @@ const contentItemSchema = z.object({
1063
1276
  metadata: z.record(z.unknown()).optional().describe("Arbitrary metadata key-value pairs"),
1064
1277
  is_premium: z.boolean().optional().describe("Whether this content requires a premium entitlement")
1065
1278
  });
1066
- function registerTools$38(server, apiClient) {
1279
+ function registerTools$39(server, apiClient) {
1067
1280
  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`.", {
1068
1281
  project_id: z.string().describe("The project ID"),
1069
1282
  name: z.string().describe("Library name (e.g. \"Daily Motivation\", \"Workout Tips\")"),
@@ -1281,7 +1494,7 @@ function registerTools$38(server, apiClient) {
1281
1494
  }
1282
1495
  //#endregion
1283
1496
  //#region src/tools/streaks.ts
1284
- function registerTools$37(server, apiClient) {
1497
+ function registerTools$38(server, apiClient) {
1285
1498
  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.", {
1286
1499
  project_id: z.string().describe("The project ID"),
1287
1500
  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`."),
@@ -1373,7 +1586,7 @@ function registerTools$37(server, apiClient) {
1373
1586
  //#endregion
1374
1587
  //#region src/tools/integrations.ts
1375
1588
  const providerEnum = z.enum(INTEGRATION_PROVIDERS);
1376
- function registerTools$36(server, apiClient) {
1589
+ function registerTools$37(server, apiClient) {
1377
1590
  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.", {
1378
1591
  project_id: z.string().describe("The project ID"),
1379
1592
  provider: providerEnum.describe("Integration provider name"),
@@ -1443,7 +1656,7 @@ const PERIOD_DAYS = {
1443
1656
  "30d": "30",
1444
1657
  "90d": "90"
1445
1658
  };
1446
- function registerTools$35(server, apiClient) {
1659
+ function registerTools$36(server, apiClient) {
1447
1660
  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.", {
1448
1661
  project_id: z.string().describe("The project ID"),
1449
1662
  period: z.enum([
@@ -1465,68 +1678,8 @@ function registerTools$35(server, apiClient) {
1465
1678
  }, ["amba_get_analytics"]);
1466
1679
  }
1467
1680
  //#endregion
1468
- //#region src/lib/tool-result.ts
1469
- /**
1470
- * Shared MCP tool result helpers.
1471
- *
1472
- * Every tool emits the same `{ content: [{ type: 'text', text: <json> }] }`
1473
- * envelope. Two helpers centralize that:
1474
- *
1475
- * - [`jsonResult`] — wraps an arbitrary payload.
1476
- * - [`passthroughResult`] — flattens an upstream HTTP response
1477
- * (status + parsed body) into the same envelope. Status is written
1478
- * LAST so a colliding top-level `status` field in the API response
1479
- * cannot shadow the HTTP status — agents look at `parsed.status` to
1480
- * distinguish 2xx from 4xx/5xx.
1481
- *
1482
- * Lives in `src/lib/` (vs. `src/tools/_helpers.ts`) to set the same
1483
- * cross-cutting-helper precedent as `src/lib/with-pat.ts` (task #36).
1484
- * `tools/*` files stay strictly tool registrations.
1485
- */
1486
- /**
1487
- * Wire-shape every MCP tool handler returns.
1488
- *
1489
- * The MCP SDK's `tool()` callback signature is structurally typed and
1490
- * carries an open index signature for `_meta` etc. Declaring our return
1491
- * type as a plain `{ content: [...] }` interface won't satisfy that
1492
- * structural check — so the helpers' return type is left as the actual
1493
- * inferred shape (no explicit interface) and consumers rely on the
1494
- * inference + the SDK's structural compatibility. If we ever want a
1495
- * named alias, write it as a type-alias over the inferred shape rather
1496
- * than a closed interface.
1497
- */
1498
- /** Wrap an arbitrary payload as a JSON-text tool result. */
1499
- function jsonResult$1(payload) {
1500
- return { content: [{
1501
- type: "text",
1502
- text: JSON.stringify(payload, null, 2)
1503
- }] };
1504
- }
1505
- /** Preserve an HTTP response wrapper while mapping failures to MCP isError. */
1506
- function httpResult(result) {
1507
- return {
1508
- ...jsonResult$1(result),
1509
- ...result.status >= 400 ? { isError: true } : {}
1510
- };
1511
- }
1512
- /**
1513
- * Flatten an upstream HTTP response into the agent-facing tool payload.
1514
- * Body fields are spread FIRST so a future top-level `status` key in
1515
- * the API response cannot shadow the HTTP `status` — agents read
1516
- * `parsed.status` to distinguish 2xx from 4xx/5xx.
1517
- */
1518
- function passthroughResult(result) {
1519
- return {
1520
- ...jsonResult$1({
1521
- ...result.body !== null && typeof result.body === "object" ? result.body : {},
1522
- status: result.status
1523
- }),
1524
- ...result.status >= 400 ? { isError: true } : {}
1525
- };
1526
- }
1527
- //#endregion
1528
1681
  //#region src/tools/users.ts
1529
- function registerTools$34(server, apiClient) {
1682
+ function registerTools$35(server, apiClient) {
1530
1683
  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.", {
1531
1684
  project_id: z.string().describe("The project ID"),
1532
1685
  limit: z.number().optional().describe("Maximum number of users to return (default 50)"),
@@ -1973,7 +2126,7 @@ final dailyLimit = await Amba.config.get<int>('daily_limit', defaultValue: 10);
1973
2126
  final streak = await Amba.streaks.get('daily_login');
1974
2127
  print('Current streak: \${streak.currentCount}');`
1975
2128
  };
1976
- function registerTools$33(server, apiClient) {
2129
+ function registerTools$34(server, apiClient) {
1977
2130
  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([
1978
2131
  "ios",
1979
2132
  "android",
@@ -2058,7 +2211,7 @@ const criteriaSchema = z.object({
2058
2211
  property_key: z.string().optional().describe("User property key (required for property_value type)"),
2059
2212
  target_value: z.number().describe("Target value to unlock the achievement")
2060
2213
  });
2061
- function registerTools$32(server, apiClient) {
2214
+ function registerTools$33(server, apiClient) {
2062
2215
  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.", {
2063
2216
  project_id: z.string().describe("The project ID"),
2064
2217
  key: z.string().describe("Unique key for this achievement (e.g. \"first_workout\", \"streak_master_7\")"),
@@ -2163,7 +2316,7 @@ function registerTools$32(server, apiClient) {
2163
2316
  }
2164
2317
  //#endregion
2165
2318
  //#region src/tools/challenges.ts
2166
- function registerTools$31(server, apiClient) {
2319
+ function registerTools$32(server, apiClient) {
2167
2320
  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.", {
2168
2321
  project_id: z.string().describe("The project ID"),
2169
2322
  name: z.string().describe("Challenge name (e.g. \"7-Day Fitness Sprint\", \"XP Weekend Blitz\")"),
@@ -2274,7 +2427,7 @@ function registerTools$31(server, apiClient) {
2274
2427
  }
2275
2428
  //#endregion
2276
2429
  //#region src/tools/economy.ts
2277
- function registerTools$30(server, apiClient) {
2430
+ function registerTools$31(server, apiClient) {
2278
2431
  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.", {
2279
2432
  project_id: z.string().describe("The project ID"),
2280
2433
  code: z.string().describe("Unique currency code (e.g. \"gold\", \"gems\", \"hearts\")"),
@@ -2965,7 +3118,7 @@ const objectTypeSchema = z.enum([
2965
3118
  "package",
2966
3119
  "paywall"
2967
3120
  ]);
2968
- function registerTools$29(server, apiClient) {
3121
+ function registerTools$30(server, apiClient) {
2969
3122
  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 }) => {
2970
3123
  const result = await client.get(`/projects/${project_id}/monetization/plan`);
2971
3124
  return { content: [{
@@ -3031,7 +3184,7 @@ function registerTools$29(server, apiClient) {
3031
3184
  }
3032
3185
  //#endregion
3033
3186
  //#region src/tools/leaderboards.ts
3034
- function registerTools$28(server, apiClient) {
3187
+ function registerTools$29(server, apiClient) {
3035
3188
  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)`.", {
3036
3189
  project_id: z.string().describe("The project ID"),
3037
3190
  name: z.string().describe("Leaderboard name (e.g. \"Top XP Earners\", \"Weekly Streak Leaders\")"),
@@ -3146,7 +3299,7 @@ function registerTools$28(server, apiClient) {
3146
3299
  * rollover (the scheduled weekly rollover) and score live off `xp_awarded`
3147
3300
  * engagement events.
3148
3301
  */
3149
- function registerTools$27(server, apiClient) {
3302
+ function registerTools$28(server, apiClient) {
3150
3303
  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.", {
3151
3304
  project_id: z.string().describe("The project ID"),
3152
3305
  name: z.string().describe("League tier name, e.g. \"Bronze\", \"Silver\", \"Gold\""),
@@ -3209,7 +3362,7 @@ function registerTools$27(server, apiClient) {
3209
3362
  }
3210
3363
  //#endregion
3211
3364
  //#region src/tools/platform.ts
3212
- function registerTools$26(server, apiClient) {
3365
+ function registerTools$27(server, apiClient) {
3213
3366
  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).", {
3214
3367
  project_id: z.string().describe("The project ID"),
3215
3368
  name: z.string().describe("Flow name (e.g. \"Welcome Flow\", \"Premium Onboarding\")"),
@@ -3862,7 +4015,7 @@ function registerTools$26(server, apiClient) {
3862
4015
  }
3863
4016
  //#endregion
3864
4017
  //#region src/tools/social.ts
3865
- function registerTools$25(server, apiClient) {
4018
+ function registerTools$26(server, apiClient) {
3866
4019
  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 }) => {
3867
4020
  const result = await client.get(`/projects/${project_id}/friends/stats`);
3868
4021
  return { content: [{
@@ -4187,7 +4340,7 @@ function registerTools$25(server, apiClient) {
4187
4340
  }
4188
4341
  //#endregion
4189
4342
  //#region src/tools/xp.ts
4190
- function registerTools$24(server, apiClient) {
4343
+ function registerTools$25(server, apiClient) {
4191
4344
  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.", {
4192
4345
  project_id: z.string().describe("The project ID"),
4193
4346
  name: z.string().describe("Rule name (e.g. \"Workout Completed\", \"Daily Login Bonus\")"),
@@ -4310,7 +4463,7 @@ function registerTools$24(server, apiClient) {
4310
4463
  }
4311
4464
  //#endregion
4312
4465
  //#region src/tools/events.ts
4313
- function registerTools$23(server, apiClient) {
4466
+ function registerTools$24(server, apiClient) {
4314
4467
  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.", {
4315
4468
  project_id: z.string().describe("The project ID"),
4316
4469
  since: z.string().optional().describe("ISO-8601 lower bound for occurred_at. Defaults to 24h ago."),
@@ -4401,7 +4554,7 @@ function registerTools$23(server, apiClient) {
4401
4554
  }
4402
4555
  //#endregion
4403
4556
  //#region src/tools/event-catalog.ts
4404
- function registerTools$22(server, apiClient) {
4557
+ function registerTools$23(server, apiClient) {
4405
4558
  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.", {
4406
4559
  project_id: z.string().describe("The project ID"),
4407
4560
  plane: z.enum(["app", "control"]).optional().describe("Filter to one plane. Omit for both.")
@@ -4471,7 +4624,7 @@ const funnelStepSchema = z.object({
4471
4624
  event: z.string().describe("Event name to match (engagement event_name)."),
4472
4625
  filters: z.array(funnelFilterSchema).optional().describe("Optional property predicates, ANDed together, that an event must satisfy.")
4473
4626
  });
4474
- function registerTools$21(server, apiClient) {
4627
+ function registerTools$22(server, apiClient) {
4475
4628
  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 }) => {
4476
4629
  const result = await client.get(`/projects/${project_id}/funnels`);
4477
4630
  return { content: [{
@@ -4568,7 +4721,10 @@ function registerTools$21(server, apiClient) {
4568
4721
  //#region src/tools/auth.ts
4569
4722
  async function authFetch(apiClient, options) {
4570
4723
  const url = `${apiClient.getApiRoot()}${options.path}`;
4571
- const headers = { Accept: "application/json" };
4724
+ const headers = {
4725
+ ...apiClient.getAuthProxyHeaders?.(),
4726
+ Accept: "application/json"
4727
+ };
4572
4728
  if (options.body !== void 0) headers["Content-Type"] = "application/json";
4573
4729
  if (options.bearer) headers["Authorization"] = `Bearer ${options.bearer}`;
4574
4730
  if (options.signupToken) headers["X-Amba-Signup-Token"] = options.signupToken;
@@ -4654,6 +4810,20 @@ const LOGIN_AGENT_INSTRUCTIONS = [
4654
4810
  "For browser-based MCP clients (Claude.ai web) that cannot read a static config file,",
4655
4811
  "direct the customer to https://mcp.amba.dev/authorize to complete the OAuth flow instead."
4656
4812
  ].join(" ");
4813
+ /** The 403 codes signup answers with when new accounts are gated. */
4814
+ const SIGNUP_ACCESS_ERROR_CODES = new Set([
4815
+ "SIGNUPS_CLOSED",
4816
+ "INVITE_REQUIRED",
4817
+ "INVITE_INVALID"
4818
+ ]);
4819
+ const SIGNUP_ACCESS_AGENT_INSTRUCTIONS = [
4820
+ "New Amba accounts are gated right now. On INVITE_REQUIRED or INVITE_INVALID, ask the human for their",
4821
+ "invite code (it starts with AMBA- and arrives in their invite email) and call this tool again with `invite_code`.",
4822
+ "On SIGNUPS_CLOSED, or when they have no code, request access for them with",
4823
+ "POST /v1/auth/developer/waitlist {\"email\",\"note\"} using their own email (or send them to",
4824
+ "`error.details.request_access_url`), tell them the invite code arrives by email, then stop.",
4825
+ "Retry only with a code the human gives you; never invent codes or switch emails."
4826
+ ].join(" ");
4657
4827
  /**
4658
4828
  * Extract a usable PAT string from a signup/login response body.
4659
4829
  *
@@ -4669,15 +4839,30 @@ function extractPat(body) {
4669
4839
  if (typeof obj.access_token === "string" && obj.access_token.length > 0) return obj.access_token;
4670
4840
  return null;
4671
4841
  }
4842
+ /** The `error.code` of a gated-signup 403 body, else null. */
4843
+ function signupAccessErrorCode(result) {
4844
+ if (result.status !== 403 || result.body === null || typeof result.body !== "object") return null;
4845
+ const code = result.body.error?.code;
4846
+ return typeof code === "string" && SIGNUP_ACCESS_ERROR_CODES.has(code) ? code : null;
4847
+ }
4672
4848
  /**
4673
4849
  * Like `passthroughResult` but enriches successful signup/login bodies
4674
4850
  * with the agent-facing `mcp_config` + `agent_instructions` block.
4675
4851
  *
4676
- * Only applied on 2xx — error bodies are returned untouched so the agent
4677
- * can still surface API-level error codes (EMAIL_EXISTS, WEAK_PASSWORD,
4678
- * INVALID_CREDENTIALS, …) unambiguously.
4852
+ * Error bodies keep their API-level codes (EMAIL_EXISTS, WEAK_PASSWORD,
4853
+ * INVALID_CREDENTIALS, …) verbatim. A gated-signup 403 (SIGNUPS_CLOSED,
4854
+ * INVITE_REQUIRED, INVITE_INVALID) additionally carries
4855
+ * `agent_instructions` describing the invite / request-access path.
4679
4856
  */
4680
4857
  function enrichedAuthResult(result, agentInstructions) {
4858
+ if (signupAccessErrorCode(result) !== null) return {
4859
+ ...jsonResult$1({
4860
+ ...result.body,
4861
+ agent_instructions: SIGNUP_ACCESS_AGENT_INSTRUCTIONS,
4862
+ status: result.status
4863
+ }),
4864
+ isError: true
4865
+ };
4681
4866
  if (result.status < 200 || result.status >= 300) return passthroughResult(result);
4682
4867
  const pat = extractPat(result.body);
4683
4868
  if (pat === null) return passthroughResult(result);
@@ -4688,7 +4873,15 @@ function enrichedAuthResult(result, agentInstructions) {
4688
4873
  status: result.status
4689
4874
  });
4690
4875
  }
4691
- function registerTools$20(server, apiClient) {
4876
+ const INVITE_CODE_PARAM = z.string().optional().describe("Platform invite code from the human's invite email (starts with AMBA-; case, spaces and dashes are ignored). Required while new accounts are invite-only; ask the human for it.");
4877
+ const SIGNUP_ACCESS_ERRORS_DESCRIPTION = [
4878
+ "While signups are gated it answers 403: INVITE_REQUIRED (no invite_code sent) or INVITE_INVALID",
4879
+ "(mistyped, expired, revoked or used up) → ask the human for their code and retry with `invite_code`;",
4880
+ "SIGNUPS_CLOSED → no new accounts right now. On SIGNUPS_CLOSED, or when the human has no code,",
4881
+ "request access via POST /v1/auth/developer/waitlist {\"email\",\"note\"} with their own email",
4882
+ "(or `error.details.request_access_url`), tell the human the invite code arrives by email, and stop."
4883
+ ].join(" ");
4884
+ function registerTools$21(server, apiClient) {
4692
4885
  registerPublicTool(server, "amba_developer_signup", [
4693
4886
  "Create a new Amba developer account. Returns a long-lived Personal Access Token (PAT)",
4694
4887
  "plus a real isolated Amba project (provisioning asynchronously), plus ready-to-paste",
@@ -4702,19 +4895,24 @@ function registerTools$20(server, apiClient) {
4702
4895
  "Poll `amba_projects_get_provisioning_status` until the project flips to status='active' (~10s)",
4703
4896
  "before issuing client-plane traffic against the returned API keys.",
4704
4897
  "Email is normalized to lowercase. Password must be at least 8 characters.",
4705
- "Errors: 409 EMAIL_EXISTS, 400 WEAK_PASSWORD/INVALID_INPUT, 429 if rate-limited (5/min, 50/day per IP)."
4898
+ "Errors: 409 EMAIL_EXISTS (the address, or a +tag / Gmail-dot alias of it, is already registered),",
4899
+ "400 DISPOSABLE_EMAIL (temporary-inbox domain; use a permanent address), 400 WEAK_PASSWORD/INVALID_INPUT,",
4900
+ "429 if rate-limited (5/min, 50/day per IP).",
4901
+ SIGNUP_ACCESS_ERRORS_DESCRIPTION
4706
4902
  ].join(" "), {
4707
4903
  email: z.string().email().describe("Developer email address. Will be lowercased."),
4708
4904
  password: z.string().min(8).describe("Password, minimum 8 characters."),
4709
4905
  name: z.string().optional().describe("Optional display name."),
4710
- 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.")
4711
- }, async ({ email, password, name, referral_code }) => {
4906
+ 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."),
4907
+ invite_code: INVITE_CODE_PARAM
4908
+ }, async ({ email, password, name, referral_code, invite_code }) => {
4712
4909
  const body = {
4713
4910
  email,
4714
4911
  password
4715
4912
  };
4716
4913
  if (name !== void 0) body.name = name;
4717
4914
  if (referral_code !== void 0) body.referral_code = referral_code;
4915
+ if (invite_code !== void 0) body.invite_code = invite_code;
4718
4916
  const signupToken = apiClient.getSignupToken();
4719
4917
  return enrichedAuthResult(await authFetch(apiClient, {
4720
4918
  method: "POST",
@@ -4730,18 +4928,21 @@ function registerTools$20(server, apiClient) {
4730
4928
  "(amba_affiliate_invite with affiliate_org_id), then set up payouts with",
4731
4929
  "amba_affiliate_my_payout_account_create. Agent: pass the returned `pat` inline on subsequent calls",
4732
4930
  "and write the mcp_config snippet to the customer's MCP config for future sessions.",
4733
- "Email is lowercased; password must be at least 8 characters."
4931
+ "Email is lowercased; password must be at least 8 characters.",
4932
+ SIGNUP_ACCESS_ERRORS_DESCRIPTION
4734
4933
  ].join(" "), {
4735
4934
  email: z.string().email().describe("Affiliate email address. Will be lowercased."),
4736
4935
  password: z.string().min(8).describe("Password, minimum 8 characters."),
4737
4936
  name: z.string().optional().describe("Optional display name."),
4738
- invite_token: z.string().optional().describe("Optional affiliate invite token (for reference).")
4739
- }, async ({ email, password, name }) => {
4937
+ invite_token: z.string().optional().describe("Optional affiliate invite token (for reference)."),
4938
+ invite_code: INVITE_CODE_PARAM
4939
+ }, async ({ email, password, name, invite_code }) => {
4740
4940
  const body = {
4741
4941
  email,
4742
4942
  password
4743
4943
  };
4744
4944
  if (name !== void 0) body.name = name;
4945
+ if (invite_code !== void 0) body.invite_code = invite_code;
4745
4946
  const signupToken = apiClient.getSignupToken();
4746
4947
  return enrichedAuthResult(await authFetch(apiClient, {
4747
4948
  method: "POST",
@@ -4756,7 +4957,8 @@ function registerTools$20(server, apiClient) {
4756
4957
  "Agent: keep working in this same session by passing `pat` as an inline arg on every",
4757
4958
  "subsequent `amba_*` tool call. Also write the matching snippet to the customer's MCP",
4758
4959
  "config file so future sessions pick the PAT up as the inbound Bearer automatically — the customer does nothing.",
4759
- "Errors: 401 INVALID_CREDENTIALS, 429 if rate-limited (10/min, 100/day per IP)."
4960
+ "Errors: 401 INVALID_CREDENTIALS, 429 if rate-limited (10/min, 100/day per IP; 20 failed",
4961
+ "attempts/hour, 100/day per account)."
4760
4962
  ].join(" "), {
4761
4963
  email: z.string().email().describe("Developer email address."),
4762
4964
  password: z.string().describe("Developer password.")
@@ -4825,6 +5027,41 @@ function registerTools$20(server, apiClient) {
4825
5027
  });
4826
5028
  }
4827
5029
  //#endregion
5030
+ //#region src/tools/developer-claim.ts
5031
+ function registerTools$20(server, apiClient) {
5032
+ registerTool(server, apiClient, "amba_developer_claim", [
5033
+ "Claim an agent-created sandbox account for its human owner. Emails a single-use link to `email`;",
5034
+ "when the owner clicks it the account takes that address, becomes verified, and any unclaimed-sandbox",
5035
+ "hold or archive on its projects lifts immediately. Call this as soon as you know who owns the app:",
5036
+ "each free project of an unclaimed sandbox gets a claim notice from day 7 of its own clock, goes on hold",
5037
+ "on day 14, and is archived (nothing deleted) on day 90. Requires the sandbox account's PAT. Errors: 409 ALREADY_CLAIMED,",
5038
+ "409 EMAIL_TAKEN, 429 when called too often."
5039
+ ].join(" "), { email: z.string().email().describe("The human owner's email address.") }, async (args, { pat }) => {
5040
+ const res = await fetch(`${apiClient.getApiRoot()}/auth/developer/claim`, {
5041
+ method: "POST",
5042
+ headers: {
5043
+ Accept: "application/json",
5044
+ "Content-Type": "application/json",
5045
+ Authorization: `Bearer ${pat}`
5046
+ },
5047
+ body: JSON.stringify({ email: args.email })
5048
+ });
5049
+ let body;
5050
+ try {
5051
+ body = await res.json();
5052
+ } catch {
5053
+ body = { error: {
5054
+ code: "INVALID_RESPONSE",
5055
+ message: res.statusText
5056
+ } };
5057
+ }
5058
+ return passthroughResult({
5059
+ status: res.status,
5060
+ body
5061
+ });
5062
+ });
5063
+ }
5064
+ //#endregion
4828
5065
  //#region src/tools/collections.ts
4829
5066
  async function adminFetch$1(apiClient, options) {
4830
5067
  let url = `${apiClient.getBaseUrl()}${options.path}`;
@@ -5742,16 +5979,7 @@ async function rawFetch(baseUrl, bearer, method, path, options) {
5742
5979
  headers,
5743
5980
  body
5744
5981
  });
5745
- if (!res.ok) {
5746
- let errorMessage = res.statusText;
5747
- let errorCode;
5748
- try {
5749
- const errBody = await res.json();
5750
- errorMessage = errBody?.error?.message ?? res.statusText;
5751
- errorCode = errBody?.error?.code;
5752
- } catch {}
5753
- throw new AmbaApiError(res.status, errorCode, errorMessage);
5754
- }
5982
+ if (!res.ok) throw await ambaApiErrorFromResponse(res);
5755
5983
  if (res.status === 204) return;
5756
5984
  return await res.json();
5757
5985
  }
@@ -6908,7 +7136,36 @@ const TIER_CATALOG = {
6908
7136
  },
6909
7137
  max_projects_per_account: 2,
6910
7138
  sleeps_after_inactivity_days: 14,
6911
- all_categories: true
7139
+ database: {
7140
+ max_compute_units: .25,
7141
+ compute_cu_hours_per_month: 10,
7142
+ storage_mb: 512,
7143
+ at_compute_cap: "database pauses until the period resets or the project upgrades",
7144
+ at_storage_cap: "writes that add data are refused; reads and deletes keep working",
7145
+ upgrade_tool: "amba_billing_upgrade"
7146
+ },
7147
+ all_categories: true,
7148
+ free_tier_limits: {
7149
+ paid_features: [
7150
+ "custom_domains",
7151
+ "domain_purchase",
7152
+ "email_send",
7153
+ "email_templates",
7154
+ "app_builder"
7155
+ ],
7156
+ auth_emails_per_day: 100,
7157
+ sms_otp_per_day: 10,
7158
+ push_campaign_sends_per_day: 3,
7159
+ push_direct_sends_per_day: 100,
7160
+ function_invocations_per_day: 1e4,
7161
+ function_cpu_ms_per_invocation: 50,
7162
+ function_schedules: 2,
7163
+ function_schedule_min_interval_minutes: 60,
7164
+ queue_messages_per_day: 1e3,
7165
+ tracked_links_per_day: 50,
7166
+ sites: "preview bar + noindex on *.app.amba.host",
7167
+ docs: "https://docs.amba.dev/billing#free-tier-limits"
7168
+ }
6912
7169
  },
6913
7170
  {
6914
7171
  name: "pro",
@@ -6922,6 +7179,11 @@ const TIER_CATALOG = {
6922
7179
  media_storage_mb: 1024
6923
7180
  },
6924
7181
  sleeps_after_inactivity_days: null,
7182
+ database: {
7183
+ max_compute_units: 1,
7184
+ compute_cu_hours_per_month: null,
7185
+ storage_mb: null
7186
+ },
6925
7187
  all_categories: true
6926
7188
  },
6927
7189
  {
@@ -6936,6 +7198,11 @@ const TIER_CATALOG = {
6936
7198
  media_storage_mb: 25600
6937
7199
  },
6938
7200
  sleeps_after_inactivity_days: null,
7201
+ database: {
7202
+ max_compute_units: 1,
7203
+ compute_cu_hours_per_month: null,
7204
+ storage_mb: null
7205
+ },
6939
7206
  all_categories: true
6940
7207
  },
6941
7208
  {
@@ -6973,7 +7240,7 @@ const TIER_CATALOG = {
6973
7240
  }
6974
7241
  };
6975
7242
  function registerTools$9(server, apiClient) {
6976
- 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 }) => {
7243
+ 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. The `database` block reports the database limits: compute used vs the free-tier cap in CU-hours (`compute.used_cu_hours`, `compute.limit_cu_hours`, `compute.pct`), storage in MB (`storage.used_mb`, `storage.limit_mb`), the reset date (`period.resets_at`), and `limit_reached`. At the compute cap the database pauses and every database-backed call returns 402 FREE_TIER_LIMIT_REACHED until the period resets or the project upgrades with amba_billing_upgrade. Use this BEFORE provisioning new features or running data-heavy workloads so the agent can self-throttle, and recommend the upgrade once `database.compute.pct` reaches 0.8.", { project_id: z.string().describe("The project ID") }, async ({ project_id }, { client }) => {
6977
7244
  const result = await client.get(`/projects/${project_id}/billing/status`);
6978
7245
  return { content: [{
6979
7246
  type: "text",
@@ -6986,6 +7253,25 @@ function registerTools$9(server, apiClient) {
6986
7253
  text: JSON.stringify(TIER_CATALOG, null, 2)
6987
7254
  }] };
6988
7255
  });
7256
+ registerTool(server, apiClient, "amba_billing_upgrade", "Move a project to a paid plan in one call: returns a checkout link for the project owner to open (the agent cannot complete payment). This is the call that lifts the free-tier database caps. Free projects run on a small database (0.25 compute units, 10 CU-hours of compute per billing month, 512 MB); at the compute cap the database pauses until the period resets, so an app with real users belongs on a paid plan, where there is no database compute cap. Call it when a request returns 402 FREE_TIER_LIMIT_REACHED (its `details.upgrade.mcp_args` carries the exact arguments), or when amba_billing_status shows `database.compute.pct` at 0.8 or more. Pro is the right default; Scale is for high-traffic apps. The plan applies as soon as checkout completes and a paused database resumes within seconds. This is a write action that leads to a charge: surface the link and the price to the human before calling, then give them the URL.", {
7257
+ project_id: z.string().describe("The project ID"),
7258
+ tier: z.enum(["pro", "scale"]).default("pro").describe("Paid plan: \"pro\" ($20/month) or \"scale\" ($200/month). Defaults to pro."),
7259
+ interval: z.enum(["month", "year"]).default("month").describe("Billing interval. Annual plans bill 12 months at a 20% discount.")
7260
+ }, async ({ project_id, tier, interval }, { client }) => {
7261
+ const url = (await client.post(`/projects/${project_id}/billing/checkout`, {
7262
+ tier: tier ?? "pro",
7263
+ interval: interval ?? "month"
7264
+ }))?.data?.url ?? null;
7265
+ return { content: [{
7266
+ type: "text",
7267
+ text: JSON.stringify({
7268
+ checkout_url: url,
7269
+ tier: tier ?? "pro",
7270
+ interval: interval ?? "month",
7271
+ next_step: url ? "Give this checkout link to the project owner. The paid plan (and the lifted database caps) apply as soon as they complete checkout." : "Checkout did not return a link; check amba_billing_status for the current subscription state."
7272
+ }, null, 2)
7273
+ }] };
7274
+ });
6989
7275
  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.", {
6990
7276
  project_id: z.string().describe("The project ID"),
6991
7277
  ceiling_usd: z.number().min(0).max(1e5).nullable().describe("Maximum monthly bill in USD, or null to disable")
@@ -9806,6 +10092,7 @@ const TOOL_CATEGORY = {
9806
10092
  amba_developer_me: "identity",
9807
10093
  amba_developer_verify: "identity",
9808
10094
  amba_developer_rotate_pat: "identity",
10095
+ amba_developer_claim: "identity",
9809
10096
  amba_users_list: "identity",
9810
10097
  amba_list_users: "identity",
9811
10098
  amba_users_create: "identity",
@@ -10290,6 +10577,7 @@ const TOOL_CATEGORY = {
10290
10577
  amba_billing_status: "infrastructure",
10291
10578
  amba_billing_tiers: "infrastructure",
10292
10579
  amba_billing_set_ceiling: "infrastructure",
10580
+ amba_billing_upgrade: "infrastructure",
10293
10581
  amba_payments_account_create: "economy",
10294
10582
  amba_payments_create_onboarding_link: "economy",
10295
10583
  amba_payments_account_status: "economy",
@@ -11280,10 +11568,12 @@ async function registerAutoMcpTools(server, apiClient, options) {
11280
11568
  * called against it once.
11281
11569
  */
11282
11570
  function registerAllTools(server, apiClient) {
11571
+ registerTools$21(server, apiClient);
11283
11572
  registerTools$20(server, apiClient);
11284
- registerTools$42(server, apiClient);
11573
+ registerTools$43(server, apiClient);
11285
11574
  registerTools$5(server, apiClient);
11286
11575
  registerTools$3(server, apiClient);
11576
+ registerTools$42(server, apiClient);
11287
11577
  registerTools$41(server, apiClient);
11288
11578
  registerTools$40(server, apiClient);
11289
11579
  registerTools$39(server, apiClient);
@@ -11304,7 +11594,6 @@ function registerAllTools(server, apiClient) {
11304
11594
  registerTools$24(server, apiClient);
11305
11595
  registerTools$23(server, apiClient);
11306
11596
  registerTools$22(server, apiClient);
11307
- registerTools$21(server, apiClient);
11308
11597
  registerTools$19(server, apiClient);
11309
11598
  registerTools$18(server, apiClient);
11310
11599
  registerTools$17(server, apiClient);
@@ -11337,6 +11626,7 @@ function createApiClient(options) {
11337
11626
  const opts = {};
11338
11627
  if (options.baseUrl !== void 0) opts.baseUrl = options.baseUrl;
11339
11628
  if (options.signupToken !== void 0) opts.signupToken = options.signupToken;
11629
+ if (options.authProxyHeaders !== void 0) opts.authProxyHeaders = options.authProxyHeaders;
11340
11630
  if (options.token !== void 0) {
11341
11631
  const token = options.token;
11342
11632
  opts.getToken = async () => token;