@layers/amba-mcp 4.0.10 → 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,24 +48,57 @@ 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;
92
+ signupToken;
63
93
  constructor(options) {
94
+ this.authProxyHeaders = { ...options?.authProxyHeaders };
64
95
  const raw = (options?.baseUrl ?? BASE_URL).replace(/\/+$/, "");
65
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).`);
66
97
  this.baseUrl = raw;
67
98
  this.apiRoot = raw.replace(/\/admin$/, "");
68
99
  this.tokenProvider = options?.getToken;
100
+ const signupToken = options?.signupToken?.trim();
101
+ if (signupToken) this.signupToken = signupToken;
69
102
  }
70
103
  /**
71
104
  * Return a sibling `ApiClient` bound to a different Bearer token.
@@ -81,10 +114,16 @@ var ApiClient = class ApiClient {
81
114
  * routing stays identical — only the token provider differs.
82
115
  */
83
116
  withToken(token) {
84
- return new ApiClient({
117
+ const opts = {
85
118
  baseUrl: this.baseUrl,
86
119
  getToken: async () => token
87
- });
120
+ };
121
+ if (this.signupToken !== void 0) opts.signupToken = this.signupToken;
122
+ return new ApiClient(opts);
123
+ }
124
+ /** The signup token to forward on `/auth/developer/signup`, if any. */
125
+ getSignupToken() {
126
+ return this.signupToken;
88
127
  }
89
128
  /**
90
129
  * Returns the configured admin-prefixed base URL (e.g.
@@ -105,6 +144,15 @@ var ApiClient = class ApiClient {
105
144
  return this.apiRoot;
106
145
  }
107
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
+ /**
108
156
  * Resolve the current developer Bearer token, going through the configured
109
157
  * tokenProvider (or `~/.amba/credentials.json` for the CLI fallback).
110
158
  *
@@ -137,6 +185,23 @@ var ApiClient = class ApiClient {
137
185
  if (isTokenExpired(credentials)) throw new Error("Developer access token has expired. Run \"amba login\" to re-authenticate.");
138
186
  return credentials.access_token;
139
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
+ }
140
205
  async request(method, path, body, query) {
141
206
  const token = await this.getToken();
142
207
  let url = `${this.baseUrl}${path}`;
@@ -154,16 +219,8 @@ var ApiClient = class ApiClient {
154
219
  headers,
155
220
  body: body !== void 0 ? JSON.stringify(body) : void 0
156
221
  });
157
- if (!res.ok) {
158
- let errorMessage = res.statusText;
159
- let errorCode;
160
- try {
161
- const errorBody = await res.json();
162
- errorMessage = errorBody?.error?.message ?? res.statusText;
163
- errorCode = errorBody?.error?.code;
164
- } catch {}
165
- throw new AmbaApiError(res.status, errorCode, errorMessage);
166
- }
222
+ this.captureNotice(res);
223
+ if (!res.ok) throw await ambaApiErrorFromResponse(res);
167
224
  if (res.status === 204) return;
168
225
  return await res.json();
169
226
  }
@@ -206,16 +263,8 @@ var ApiClient = class ApiClient {
206
263
  Accept: "text/csv, application/x-ndjson, */*"
207
264
  }
208
265
  });
209
- if (!res.ok) {
210
- let errorMessage = res.statusText;
211
- let errorCode;
212
- try {
213
- const errorBody = await res.json();
214
- errorMessage = errorBody?.error?.message ?? res.statusText;
215
- errorCode = errorBody?.error?.code;
216
- } catch {}
217
- throw new AmbaApiError(res.status, errorCode, errorMessage);
218
- }
266
+ this.captureNotice(res);
267
+ if (!res.ok) throw await ambaApiErrorFromResponse(res);
219
268
  const contentType = res.headers.get("content-type");
220
269
  const reader = res.body?.getReader();
221
270
  if (!reader) {
@@ -378,6 +427,7 @@ const WRITE_VERBS = new Set([
378
427
  "insert",
379
428
  "invite",
380
429
  "register",
430
+ "claim",
381
431
  "define",
382
432
  "map",
383
433
  "adopt",
@@ -394,7 +444,8 @@ const WRITE_VERBS = new Set([
394
444
  "generate",
395
445
  "enroll",
396
446
  "request",
397
- "clawback"
447
+ "clawback",
448
+ "upgrade"
398
449
  ]);
399
450
  /**
400
451
  * Split a tool name into lowercase verb-candidate tokens, dropping the
@@ -553,8 +604,174 @@ function deriveToolAnnotations(name) {
553
604
  }
554
605
  }
555
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
556
699
  //#region src/lib/with-pat.ts
557
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
+ /**
558
775
  * The `pat` schema entry injected into every tool that goes through
559
776
  * `registerTool`. Exported so tests can assert on its presence.
560
777
  */
@@ -569,26 +786,30 @@ const PAT_ARG_SCHEMA = z.string().optional().describe([
569
786
  ].join(" "));
570
787
  /**
571
788
  * Build the MISSING_PAT short-circuit payload. Shape matches the auth
572
- * 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).
573
791
  */
574
792
  function missingPatResult(toolName) {
575
- return { content: [{
576
- type: "text",
577
- text: JSON.stringify({
578
- status: 401,
579
- error: {
580
- code: "MISSING_PAT",
581
- message: [
582
- `Tool "${toolName}" requires a developer Bearer.`,
583
- "Pass `pat` as a tool argument (the value returned by",
584
- "`amba_developer_signup` / `amba_developer_login`). The next",
585
- "agent session will pick the PAT up automatically once the",
586
- "snippet in `mcp_config` is written to the customer's MCP",
587
- "client config, at which point the `pat` arg becomes optional."
588
- ].join(" ")
589
- }
590
- }, null, 2)
591
- }] };
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
+ };
592
813
  }
593
814
  /**
594
815
  * Register a tool that:
@@ -624,10 +845,10 @@ function registerTool(server, apiClient, name, description, schema, handler, ali
624
845
  client = apiClient;
625
846
  }
626
847
  if (!pat) return missingPatResult(registeredName);
627
- return handler(rawArgs, {
848
+ return withAccountNotices(await structuredApiErrors(() => handler(rawArgs, {
628
849
  pat,
629
850
  client
630
- });
851
+ })), client);
631
852
  };
632
853
  server.tool(name, description, extendedSchema, deriveToolAnnotations(name), wrappedHandler(name));
633
854
  for (const alias of aliases) server.tool(alias, description, extendedSchema, deriveToolAnnotations(alias), (async (rawArgs) => {
@@ -642,15 +863,16 @@ function registerTool(server, apiClient, name, description, schema, handler, ali
642
863
  * should go through `registerTool`.
643
864
  */
644
865
  function registerPublicTool(server, name, description, schema, handler, aliases = [], annotations) {
645
- 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);
646
868
  for (const alias of aliases) server.tool(alias, description, schema, annotations ?? deriveToolAnnotations(alias), (async (args) => {
647
869
  warnDeprecatedAlias(alias, name);
648
- return handler(args);
870
+ return wrappedHandler(args);
649
871
  }));
650
872
  }
651
873
  //#endregion
652
874
  //#region src/tools/projects.ts
653
- function registerTools$42(server, apiClient) {
875
+ function registerTools$43(server, apiClient) {
654
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 }) => {
655
877
  const result = await client.get("/projects");
656
878
  return { content: [{
@@ -761,7 +983,7 @@ function registerTools$42(server, apiClient) {
761
983
  }
762
984
  //#endregion
763
985
  //#region src/tools/push.ts
764
- function registerTools$41(server, apiClient) {
986
+ function registerTools$42(server, apiClient) {
765
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.", {
766
988
  project_id: z.string().describe("The project ID"),
767
989
  title: z.string().describe("Push notification title shown to the user"),
@@ -891,7 +1113,7 @@ const segmentRulesSchema = z.object({
891
1113
  operator: z.enum(["AND", "OR"]).describe("Logical operator combining conditions"),
892
1114
  conditions: z.array(segmentConditionSchema).describe("Array of filter conditions")
893
1115
  });
894
- function registerTools$40(server, apiClient) {
1116
+ function registerTools$41(server, apiClient) {
895
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\").", {
896
1118
  project_id: z.string().describe("The project ID"),
897
1119
  include_system: z.boolean().optional().describe("Include built-in system segments in the result. Defaults to false.")
@@ -976,7 +1198,7 @@ const configConditionSchema = z.object({
976
1198
  percentage: z.number().optional().describe("Percentage rollout (0-100)"),
977
1199
  value: z.unknown().describe("Override value for this condition")
978
1200
  });
979
- function registerTools$39(server, apiClient) {
1201
+ function registerTools$40(server, apiClient) {
980
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).", {
981
1203
  project_id: z.string().describe("The project ID"),
982
1204
  include_system: z.boolean().optional().describe("Include platform-provided default config keys in the result. Defaults to false.")
@@ -1054,7 +1276,7 @@ const contentItemSchema = z.object({
1054
1276
  metadata: z.record(z.unknown()).optional().describe("Arbitrary metadata key-value pairs"),
1055
1277
  is_premium: z.boolean().optional().describe("Whether this content requires a premium entitlement")
1056
1278
  });
1057
- function registerTools$38(server, apiClient) {
1279
+ function registerTools$39(server, apiClient) {
1058
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`.", {
1059
1281
  project_id: z.string().describe("The project ID"),
1060
1282
  name: z.string().describe("Library name (e.g. \"Daily Motivation\", \"Workout Tips\")"),
@@ -1272,7 +1494,7 @@ function registerTools$38(server, apiClient) {
1272
1494
  }
1273
1495
  //#endregion
1274
1496
  //#region src/tools/streaks.ts
1275
- function registerTools$37(server, apiClient) {
1497
+ function registerTools$38(server, apiClient) {
1276
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.", {
1277
1499
  project_id: z.string().describe("The project ID"),
1278
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`."),
@@ -1364,7 +1586,7 @@ function registerTools$37(server, apiClient) {
1364
1586
  //#endregion
1365
1587
  //#region src/tools/integrations.ts
1366
1588
  const providerEnum = z.enum(INTEGRATION_PROVIDERS);
1367
- function registerTools$36(server, apiClient) {
1589
+ function registerTools$37(server, apiClient) {
1368
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.", {
1369
1591
  project_id: z.string().describe("The project ID"),
1370
1592
  provider: providerEnum.describe("Integration provider name"),
@@ -1434,7 +1656,7 @@ const PERIOD_DAYS = {
1434
1656
  "30d": "30",
1435
1657
  "90d": "90"
1436
1658
  };
1437
- function registerTools$35(server, apiClient) {
1659
+ function registerTools$36(server, apiClient) {
1438
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.", {
1439
1661
  project_id: z.string().describe("The project ID"),
1440
1662
  period: z.enum([
@@ -1456,68 +1678,8 @@ function registerTools$35(server, apiClient) {
1456
1678
  }, ["amba_get_analytics"]);
1457
1679
  }
1458
1680
  //#endregion
1459
- //#region src/lib/tool-result.ts
1460
- /**
1461
- * Shared MCP tool result helpers.
1462
- *
1463
- * Every tool emits the same `{ content: [{ type: 'text', text: <json> }] }`
1464
- * envelope. Two helpers centralize that:
1465
- *
1466
- * - [`jsonResult`] — wraps an arbitrary payload.
1467
- * - [`passthroughResult`] — flattens an upstream HTTP response
1468
- * (status + parsed body) into the same envelope. Status is written
1469
- * LAST so a colliding top-level `status` field in the API response
1470
- * cannot shadow the HTTP status — agents look at `parsed.status` to
1471
- * distinguish 2xx from 4xx/5xx.
1472
- *
1473
- * Lives in `src/lib/` (vs. `src/tools/_helpers.ts`) to set the same
1474
- * cross-cutting-helper precedent as `src/lib/with-pat.ts` (task #36).
1475
- * `tools/*` files stay strictly tool registrations.
1476
- */
1477
- /**
1478
- * Wire-shape every MCP tool handler returns.
1479
- *
1480
- * The MCP SDK's `tool()` callback signature is structurally typed and
1481
- * carries an open index signature for `_meta` etc. Declaring our return
1482
- * type as a plain `{ content: [...] }` interface won't satisfy that
1483
- * structural check — so the helpers' return type is left as the actual
1484
- * inferred shape (no explicit interface) and consumers rely on the
1485
- * inference + the SDK's structural compatibility. If we ever want a
1486
- * named alias, write it as a type-alias over the inferred shape rather
1487
- * than a closed interface.
1488
- */
1489
- /** Wrap an arbitrary payload as a JSON-text tool result. */
1490
- function jsonResult$1(payload) {
1491
- return { content: [{
1492
- type: "text",
1493
- text: JSON.stringify(payload, null, 2)
1494
- }] };
1495
- }
1496
- /** Preserve an HTTP response wrapper while mapping failures to MCP isError. */
1497
- function httpResult(result) {
1498
- return {
1499
- ...jsonResult$1(result),
1500
- ...result.status >= 400 ? { isError: true } : {}
1501
- };
1502
- }
1503
- /**
1504
- * Flatten an upstream HTTP response into the agent-facing tool payload.
1505
- * Body fields are spread FIRST so a future top-level `status` key in
1506
- * the API response cannot shadow the HTTP `status` — agents read
1507
- * `parsed.status` to distinguish 2xx from 4xx/5xx.
1508
- */
1509
- function passthroughResult(result) {
1510
- return {
1511
- ...jsonResult$1({
1512
- ...result.body !== null && typeof result.body === "object" ? result.body : {},
1513
- status: result.status
1514
- }),
1515
- ...result.status >= 400 ? { isError: true } : {}
1516
- };
1517
- }
1518
- //#endregion
1519
1681
  //#region src/tools/users.ts
1520
- function registerTools$34(server, apiClient) {
1682
+ function registerTools$35(server, apiClient) {
1521
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.", {
1522
1684
  project_id: z.string().describe("The project ID"),
1523
1685
  limit: z.number().optional().describe("Maximum number of users to return (default 50)"),
@@ -1964,7 +2126,7 @@ final dailyLimit = await Amba.config.get<int>('daily_limit', defaultValue: 10);
1964
2126
  final streak = await Amba.streaks.get('daily_login');
1965
2127
  print('Current streak: \${streak.currentCount}');`
1966
2128
  };
1967
- function registerTools$33(server, apiClient) {
2129
+ function registerTools$34(server, apiClient) {
1968
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([
1969
2131
  "ios",
1970
2132
  "android",
@@ -2049,7 +2211,7 @@ const criteriaSchema = z.object({
2049
2211
  property_key: z.string().optional().describe("User property key (required for property_value type)"),
2050
2212
  target_value: z.number().describe("Target value to unlock the achievement")
2051
2213
  });
2052
- function registerTools$32(server, apiClient) {
2214
+ function registerTools$33(server, apiClient) {
2053
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.", {
2054
2216
  project_id: z.string().describe("The project ID"),
2055
2217
  key: z.string().describe("Unique key for this achievement (e.g. \"first_workout\", \"streak_master_7\")"),
@@ -2154,7 +2316,7 @@ function registerTools$32(server, apiClient) {
2154
2316
  }
2155
2317
  //#endregion
2156
2318
  //#region src/tools/challenges.ts
2157
- function registerTools$31(server, apiClient) {
2319
+ function registerTools$32(server, apiClient) {
2158
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.", {
2159
2321
  project_id: z.string().describe("The project ID"),
2160
2322
  name: z.string().describe("Challenge name (e.g. \"7-Day Fitness Sprint\", \"XP Weekend Blitz\")"),
@@ -2265,7 +2427,7 @@ function registerTools$31(server, apiClient) {
2265
2427
  }
2266
2428
  //#endregion
2267
2429
  //#region src/tools/economy.ts
2268
- function registerTools$30(server, apiClient) {
2430
+ function registerTools$31(server, apiClient) {
2269
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.", {
2270
2432
  project_id: z.string().describe("The project ID"),
2271
2433
  code: z.string().describe("Unique currency code (e.g. \"gold\", \"gems\", \"hearts\")"),
@@ -2956,7 +3118,7 @@ const objectTypeSchema = z.enum([
2956
3118
  "package",
2957
3119
  "paywall"
2958
3120
  ]);
2959
- function registerTools$29(server, apiClient) {
3121
+ function registerTools$30(server, apiClient) {
2960
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 }) => {
2961
3123
  const result = await client.get(`/projects/${project_id}/monetization/plan`);
2962
3124
  return { content: [{
@@ -3022,7 +3184,7 @@ function registerTools$29(server, apiClient) {
3022
3184
  }
3023
3185
  //#endregion
3024
3186
  //#region src/tools/leaderboards.ts
3025
- function registerTools$28(server, apiClient) {
3187
+ function registerTools$29(server, apiClient) {
3026
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)`.", {
3027
3189
  project_id: z.string().describe("The project ID"),
3028
3190
  name: z.string().describe("Leaderboard name (e.g. \"Top XP Earners\", \"Weekly Streak Leaders\")"),
@@ -3137,7 +3299,7 @@ function registerTools$28(server, apiClient) {
3137
3299
  * rollover (the scheduled weekly rollover) and score live off `xp_awarded`
3138
3300
  * engagement events.
3139
3301
  */
3140
- function registerTools$27(server, apiClient) {
3302
+ function registerTools$28(server, apiClient) {
3141
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.", {
3142
3304
  project_id: z.string().describe("The project ID"),
3143
3305
  name: z.string().describe("League tier name, e.g. \"Bronze\", \"Silver\", \"Gold\""),
@@ -3200,7 +3362,7 @@ function registerTools$27(server, apiClient) {
3200
3362
  }
3201
3363
  //#endregion
3202
3364
  //#region src/tools/platform.ts
3203
- function registerTools$26(server, apiClient) {
3365
+ function registerTools$27(server, apiClient) {
3204
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).", {
3205
3367
  project_id: z.string().describe("The project ID"),
3206
3368
  name: z.string().describe("Flow name (e.g. \"Welcome Flow\", \"Premium Onboarding\")"),
@@ -3853,7 +4015,7 @@ function registerTools$26(server, apiClient) {
3853
4015
  }
3854
4016
  //#endregion
3855
4017
  //#region src/tools/social.ts
3856
- function registerTools$25(server, apiClient) {
4018
+ function registerTools$26(server, apiClient) {
3857
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 }) => {
3858
4020
  const result = await client.get(`/projects/${project_id}/friends/stats`);
3859
4021
  return { content: [{
@@ -4178,7 +4340,7 @@ function registerTools$25(server, apiClient) {
4178
4340
  }
4179
4341
  //#endregion
4180
4342
  //#region src/tools/xp.ts
4181
- function registerTools$24(server, apiClient) {
4343
+ function registerTools$25(server, apiClient) {
4182
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.", {
4183
4345
  project_id: z.string().describe("The project ID"),
4184
4346
  name: z.string().describe("Rule name (e.g. \"Workout Completed\", \"Daily Login Bonus\")"),
@@ -4301,7 +4463,7 @@ function registerTools$24(server, apiClient) {
4301
4463
  }
4302
4464
  //#endregion
4303
4465
  //#region src/tools/events.ts
4304
- function registerTools$23(server, apiClient) {
4466
+ function registerTools$24(server, apiClient) {
4305
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.", {
4306
4468
  project_id: z.string().describe("The project ID"),
4307
4469
  since: z.string().optional().describe("ISO-8601 lower bound for occurred_at. Defaults to 24h ago."),
@@ -4392,7 +4554,7 @@ function registerTools$23(server, apiClient) {
4392
4554
  }
4393
4555
  //#endregion
4394
4556
  //#region src/tools/event-catalog.ts
4395
- function registerTools$22(server, apiClient) {
4557
+ function registerTools$23(server, apiClient) {
4396
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.", {
4397
4559
  project_id: z.string().describe("The project ID"),
4398
4560
  plane: z.enum(["app", "control"]).optional().describe("Filter to one plane. Omit for both.")
@@ -4462,7 +4624,7 @@ const funnelStepSchema = z.object({
4462
4624
  event: z.string().describe("Event name to match (engagement event_name)."),
4463
4625
  filters: z.array(funnelFilterSchema).optional().describe("Optional property predicates, ANDed together, that an event must satisfy.")
4464
4626
  });
4465
- function registerTools$21(server, apiClient) {
4627
+ function registerTools$22(server, apiClient) {
4466
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 }) => {
4467
4629
  const result = await client.get(`/projects/${project_id}/funnels`);
4468
4630
  return { content: [{
@@ -4559,9 +4721,13 @@ function registerTools$21(server, apiClient) {
4559
4721
  //#region src/tools/auth.ts
4560
4722
  async function authFetch(apiClient, options) {
4561
4723
  const url = `${apiClient.getApiRoot()}${options.path}`;
4562
- const headers = { Accept: "application/json" };
4724
+ const headers = {
4725
+ ...apiClient.getAuthProxyHeaders?.(),
4726
+ Accept: "application/json"
4727
+ };
4563
4728
  if (options.body !== void 0) headers["Content-Type"] = "application/json";
4564
4729
  if (options.bearer) headers["Authorization"] = `Bearer ${options.bearer}`;
4730
+ if (options.signupToken) headers["X-Amba-Signup-Token"] = options.signupToken;
4565
4731
  const res = await fetch(url, {
4566
4732
  method: options.method,
4567
4733
  headers,
@@ -4644,6 +4810,20 @@ const LOGIN_AGENT_INSTRUCTIONS = [
4644
4810
  "For browser-based MCP clients (Claude.ai web) that cannot read a static config file,",
4645
4811
  "direct the customer to https://mcp.amba.dev/authorize to complete the OAuth flow instead."
4646
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(" ");
4647
4827
  /**
4648
4828
  * Extract a usable PAT string from a signup/login response body.
4649
4829
  *
@@ -4659,15 +4839,30 @@ function extractPat(body) {
4659
4839
  if (typeof obj.access_token === "string" && obj.access_token.length > 0) return obj.access_token;
4660
4840
  return null;
4661
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
+ }
4662
4848
  /**
4663
4849
  * Like `passthroughResult` but enriches successful signup/login bodies
4664
4850
  * with the agent-facing `mcp_config` + `agent_instructions` block.
4665
4851
  *
4666
- * Only applied on 2xx — error bodies are returned untouched so the agent
4667
- * can still surface API-level error codes (EMAIL_EXISTS, WEAK_PASSWORD,
4668
- * 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.
4669
4856
  */
4670
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
+ };
4671
4866
  if (result.status < 200 || result.status >= 300) return passthroughResult(result);
4672
4867
  const pat = extractPat(result.body);
4673
4868
  if (pat === null) return passthroughResult(result);
@@ -4678,7 +4873,15 @@ function enrichedAuthResult(result, agentInstructions) {
4678
4873
  status: result.status
4679
4874
  });
4680
4875
  }
4681
- 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) {
4682
4885
  registerPublicTool(server, "amba_developer_signup", [
4683
4886
  "Create a new Amba developer account. Returns a long-lived Personal Access Token (PAT)",
4684
4887
  "plus a real isolated Amba project (provisioning asynchronously), plus ready-to-paste",
@@ -4692,23 +4895,30 @@ function registerTools$20(server, apiClient) {
4692
4895
  "Poll `amba_projects_get_provisioning_status` until the project flips to status='active' (~10s)",
4693
4896
  "before issuing client-plane traffic against the returned API keys.",
4694
4897
  "Email is normalized to lowercase. Password must be at least 8 characters.",
4695
- "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
4696
4902
  ].join(" "), {
4697
4903
  email: z.string().email().describe("Developer email address. Will be lowercased."),
4698
4904
  password: z.string().min(8).describe("Password, minimum 8 characters."),
4699
4905
  name: z.string().optional().describe("Optional display name."),
4700
- 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.")
4701
- }, 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 }) => {
4702
4909
  const body = {
4703
4910
  email,
4704
4911
  password
4705
4912
  };
4706
4913
  if (name !== void 0) body.name = name;
4707
4914
  if (referral_code !== void 0) body.referral_code = referral_code;
4915
+ if (invite_code !== void 0) body.invite_code = invite_code;
4916
+ const signupToken = apiClient.getSignupToken();
4708
4917
  return enrichedAuthResult(await authFetch(apiClient, {
4709
4918
  method: "POST",
4710
4919
  path: "/auth/developer/signup",
4711
- body
4920
+ body,
4921
+ ...signupToken ? { signupToken } : {}
4712
4922
  }), SIGNUP_AGENT_INSTRUCTIONS);
4713
4923
  });
4714
4924
  registerPublicTool(server, "amba_affiliate_signup", [
@@ -4718,22 +4928,27 @@ function registerTools$20(server, apiClient) {
4718
4928
  "(amba_affiliate_invite with affiliate_org_id), then set up payouts with",
4719
4929
  "amba_affiliate_my_payout_account_create. Agent: pass the returned `pat` inline on subsequent calls",
4720
4930
  "and write the mcp_config snippet to the customer's MCP config for future sessions.",
4721
- "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
4722
4933
  ].join(" "), {
4723
4934
  email: z.string().email().describe("Affiliate email address. Will be lowercased."),
4724
4935
  password: z.string().min(8).describe("Password, minimum 8 characters."),
4725
4936
  name: z.string().optional().describe("Optional display name."),
4726
- invite_token: z.string().optional().describe("Optional affiliate invite token (for reference).")
4727
- }, 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 }) => {
4728
4940
  const body = {
4729
4941
  email,
4730
4942
  password
4731
4943
  };
4732
4944
  if (name !== void 0) body.name = name;
4945
+ if (invite_code !== void 0) body.invite_code = invite_code;
4946
+ const signupToken = apiClient.getSignupToken();
4733
4947
  return enrichedAuthResult(await authFetch(apiClient, {
4734
4948
  method: "POST",
4735
4949
  path: "/auth/developer/signup",
4736
- body
4950
+ body,
4951
+ ...signupToken ? { signupToken } : {}
4737
4952
  }), SIGNUP_AGENT_INSTRUCTIONS);
4738
4953
  });
4739
4954
  registerPublicTool(server, "amba_developer_login", [
@@ -4742,7 +4957,8 @@ function registerTools$20(server, apiClient) {
4742
4957
  "Agent: keep working in this same session by passing `pat` as an inline arg on every",
4743
4958
  "subsequent `amba_*` tool call. Also write the matching snippet to the customer's MCP",
4744
4959
  "config file so future sessions pick the PAT up as the inbound Bearer automatically — the customer does nothing.",
4745
- "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)."
4746
4962
  ].join(" "), {
4747
4963
  email: z.string().email().describe("Developer email address."),
4748
4964
  password: z.string().describe("Developer password.")
@@ -4811,6 +5027,41 @@ function registerTools$20(server, apiClient) {
4811
5027
  });
4812
5028
  }
4813
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
4814
5065
  //#region src/tools/collections.ts
4815
5066
  async function adminFetch$1(apiClient, options) {
4816
5067
  let url = `${apiClient.getBaseUrl()}${options.path}`;
@@ -5728,16 +5979,7 @@ async function rawFetch(baseUrl, bearer, method, path, options) {
5728
5979
  headers,
5729
5980
  body
5730
5981
  });
5731
- if (!res.ok) {
5732
- let errorMessage = res.statusText;
5733
- let errorCode;
5734
- try {
5735
- const errBody = await res.json();
5736
- errorMessage = errBody?.error?.message ?? res.statusText;
5737
- errorCode = errBody?.error?.code;
5738
- } catch {}
5739
- throw new AmbaApiError(res.status, errorCode, errorMessage);
5740
- }
5982
+ if (!res.ok) throw await ambaApiErrorFromResponse(res);
5741
5983
  if (res.status === 204) return;
5742
5984
  return await res.json();
5743
5985
  }
@@ -5946,6 +6188,7 @@ function registerTools$17(server, apiClient) {
5946
6188
  "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.",
5947
6189
  "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`.",
5948
6190
  "Site name must match /^[a-z][a-z0-9_-]{0,49}$/. Per-file cap: 25 MiB. The site's public URL is `https://{slug}.app.amba.host` where `slug` is `{first 8 chars of project_id}-{name}`.",
6191
+ "Routing follows common static-host conventions, so directory-style exports work as-is: `/a/` serves `a/index.html`; `/a` serves the exact file, then `a.html`, then 308-redirects to `/a/` when only `a/index.html` exists. Include a top-level `404.html` to answer unknown paths with status 404; without one, extensionless paths fall back to `index.html` (single-page-app mode).",
5949
6192
  "Returns the deployment record + public URL + file/byte counts."
5950
6193
  ].join(" "), {
5951
6194
  project_id: z.string().describe("The Amba project ID."),
@@ -6197,11 +6440,12 @@ function registerTools$14(server, apiClient) {
6197
6440
  "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).",
6198
6441
  "Secret name must match /^[A-Z][A-Z0-9_]{0,62}$/. Function name (when given) must match /^[a-z][a-z0-9_-]{0,57}$/.",
6199
6442
  "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.",
6443
+ "Every function already receives AMBA_API_URL, AMBA_PROJECT_ID, AMBA_INTERNAL_TOKEN (project-scoped admin token), and AMBA_AI_GATEWAY_URL in `env`, so never set those. For your own values, use an app prefix: store the project client key as APP_CLIENT_KEY, for example.",
6200
6444
  "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.",
6201
6445
  "Value cap: 64 KiB. Plaintext is never returned afterwards."
6202
6446
  ].join(" "), {
6203
6447
  project_id: z.string().describe("The Amba project ID."),
6204
- 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."),
6448
+ 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; use an app prefix such as APP_ (APP_CLIENT_KEY)."),
6205
6449
  value: z.string().describe("Secret value. Stored encrypted at rest; never returned in plaintext afterwards."),
6206
6450
  function: z.string().optional().describe("Function this secret binds to. OMIT for a project-wide secret (applied to every function in the project).")
6207
6451
  }, async ({ project_id, name, value, function: functionName }, { pat }) => {
@@ -6892,7 +7136,36 @@ const TIER_CATALOG = {
6892
7136
  },
6893
7137
  max_projects_per_account: 2,
6894
7138
  sleeps_after_inactivity_days: 14,
6895
- 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
+ }
6896
7169
  },
6897
7170
  {
6898
7171
  name: "pro",
@@ -6906,6 +7179,11 @@ const TIER_CATALOG = {
6906
7179
  media_storage_mb: 1024
6907
7180
  },
6908
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
+ },
6909
7187
  all_categories: true
6910
7188
  },
6911
7189
  {
@@ -6920,6 +7198,11 @@ const TIER_CATALOG = {
6920
7198
  media_storage_mb: 25600
6921
7199
  },
6922
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
+ },
6923
7206
  all_categories: true
6924
7207
  },
6925
7208
  {
@@ -6957,7 +7240,7 @@ const TIER_CATALOG = {
6957
7240
  }
6958
7241
  };
6959
7242
  function registerTools$9(server, apiClient) {
6960
- 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 }) => {
6961
7244
  const result = await client.get(`/projects/${project_id}/billing/status`);
6962
7245
  return { content: [{
6963
7246
  type: "text",
@@ -6970,6 +7253,25 @@ function registerTools$9(server, apiClient) {
6970
7253
  text: JSON.stringify(TIER_CATALOG, null, 2)
6971
7254
  }] };
6972
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
+ });
6973
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.", {
6974
7276
  project_id: z.string().describe("The project ID"),
6975
7277
  ceiling_usd: z.number().min(0).max(1e5).nullable().describe("Maximum monthly bill in USD, or null to disable")
@@ -9790,6 +10092,7 @@ const TOOL_CATEGORY = {
9790
10092
  amba_developer_me: "identity",
9791
10093
  amba_developer_verify: "identity",
9792
10094
  amba_developer_rotate_pat: "identity",
10095
+ amba_developer_claim: "identity",
9793
10096
  amba_users_list: "identity",
9794
10097
  amba_list_users: "identity",
9795
10098
  amba_users_create: "identity",
@@ -10274,6 +10577,7 @@ const TOOL_CATEGORY = {
10274
10577
  amba_billing_status: "infrastructure",
10275
10578
  amba_billing_tiers: "infrastructure",
10276
10579
  amba_billing_set_ceiling: "infrastructure",
10580
+ amba_billing_upgrade: "infrastructure",
10277
10581
  amba_payments_account_create: "economy",
10278
10582
  amba_payments_create_onboarding_link: "economy",
10279
10583
  amba_payments_account_status: "economy",
@@ -11264,10 +11568,12 @@ async function registerAutoMcpTools(server, apiClient, options) {
11264
11568
  * called against it once.
11265
11569
  */
11266
11570
  function registerAllTools(server, apiClient) {
11571
+ registerTools$21(server, apiClient);
11267
11572
  registerTools$20(server, apiClient);
11268
- registerTools$42(server, apiClient);
11573
+ registerTools$43(server, apiClient);
11269
11574
  registerTools$5(server, apiClient);
11270
11575
  registerTools$3(server, apiClient);
11576
+ registerTools$42(server, apiClient);
11271
11577
  registerTools$41(server, apiClient);
11272
11578
  registerTools$40(server, apiClient);
11273
11579
  registerTools$39(server, apiClient);
@@ -11288,7 +11594,6 @@ function registerAllTools(server, apiClient) {
11288
11594
  registerTools$24(server, apiClient);
11289
11595
  registerTools$23(server, apiClient);
11290
11596
  registerTools$22(server, apiClient);
11291
- registerTools$21(server, apiClient);
11292
11597
  registerTools$19(server, apiClient);
11293
11598
  registerTools$18(server, apiClient);
11294
11599
  registerTools$17(server, apiClient);
@@ -11320,6 +11625,8 @@ function registerAllTools(server, apiClient) {
11320
11625
  function createApiClient(options) {
11321
11626
  const opts = {};
11322
11627
  if (options.baseUrl !== void 0) opts.baseUrl = options.baseUrl;
11628
+ if (options.signupToken !== void 0) opts.signupToken = options.signupToken;
11629
+ if (options.authProxyHeaders !== void 0) opts.authProxyHeaders = options.authProxyHeaders;
11323
11630
  if (options.token !== void 0) {
11324
11631
  const token = options.token;
11325
11632
  opts.getToken = async () => token;