@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/README.md +1 -1
- package/dist/api-client.d.ts +43 -1
- package/dist/expo-build-prompt.js +4 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +442 -152
- package/dist/lib/tool-result.d.ts +30 -0
- package/dist/lib/with-pat.d.ts +11 -1
- package/dist/resources/amba-setup.d.ts +1 -1
- package/dist/tools/billing.d.ts +5 -1
- package/dist/tools/developer-claim.d.ts +15 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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 {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
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
|
|
870
|
+
return wrappedHandler(args);
|
|
658
871
|
}));
|
|
659
872
|
}
|
|
660
873
|
//#endregion
|
|
661
874
|
//#region src/tools/projects.ts
|
|
662
|
-
function registerTools$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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 = {
|
|
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
|
-
*
|
|
4677
|
-
*
|
|
4678
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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$
|
|
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;
|