@layers/amba-mcp 0.1.1 → 1.0.0
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/api-client.d.ts +66 -0
- package/dist/auth.d.ts +7 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +3230 -0
- package/dist/tools/achievements.d.ts +3 -0
- package/dist/tools/analytics.d.ts +3 -0
- package/dist/tools/auth.d.ts +37 -0
- package/dist/tools/challenges.d.ts +3 -0
- package/dist/tools/config.d.ts +3 -0
- package/dist/tools/content.d.ts +3 -0
- package/dist/tools/economy.d.ts +3 -0
- package/dist/tools/events.d.ts +3 -0
- package/dist/tools/integrations.d.ts +3 -0
- package/dist/tools/leaderboards.d.ts +3 -0
- package/dist/tools/platform.d.ts +3 -0
- package/dist/tools/projects.d.ts +3 -0
- package/dist/tools/push.d.ts +3 -0
- package/dist/tools/segments.d.ts +3 -0
- package/dist/tools/setup.d.ts +3 -0
- package/dist/tools/social.d.ts +3 -0
- package/dist/tools/streaks.d.ts +3 -0
- package/dist/tools/users.d.ts +3 -0
- package/dist/tools/xp.d.ts +3 -0
- package/package.json +7 -6
package/dist/index.js
ADDED
|
@@ -0,0 +1,3230 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { INTEGRATION_PROVIDERS } from "@layers/amba-shared";
|
|
6
|
+
//#region src/auth.ts
|
|
7
|
+
const CREDENTIALS_PATH = join(homedir(), ".amba", "credentials.json");
|
|
8
|
+
async function loadCredentials() {
|
|
9
|
+
try {
|
|
10
|
+
const raw = await readFile(CREDENTIALS_PATH, "utf-8");
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
if (typeof parsed !== "object" || parsed === null || !("access_token" in parsed) || !("refresh_token" in parsed) || !("expires_at" in parsed)) throw new Error("Invalid credentials format");
|
|
13
|
+
const creds = parsed;
|
|
14
|
+
if (!creds.access_token || typeof creds.access_token !== "string") throw new Error("Missing or invalid access_token in credentials");
|
|
15
|
+
return creds;
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") throw new Error(`Credentials file not found at ${CREDENTIALS_PATH}. Run "amba login" to authenticate first.`);
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isTokenExpired(credentials) {
|
|
22
|
+
if (!credentials.expires_at) return false;
|
|
23
|
+
const expiresAt = new Date(credentials.expires_at).getTime();
|
|
24
|
+
return Date.now() > expiresAt - 6e4;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/api-client.ts
|
|
28
|
+
const BASE_URL = "https://api.amba.dev/v1/admin";
|
|
29
|
+
var ApiClient = class {
|
|
30
|
+
baseUrl;
|
|
31
|
+
apiRoot;
|
|
32
|
+
tokenProvider;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
const raw = (options?.baseUrl ?? BASE_URL).replace(/\/+$/, "");
|
|
35
|
+
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).`);
|
|
36
|
+
this.baseUrl = raw;
|
|
37
|
+
this.apiRoot = raw.replace(/\/admin$/, "");
|
|
38
|
+
this.tokenProvider = options?.getToken;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns the configured admin-prefixed base URL (e.g.
|
|
42
|
+
* `https://api.amba.dev/admin`). Auth tools (`amba_developer_signup`,
|
|
43
|
+
* `amba_developer_login`, `amba_developer_refresh`) need a sibling root
|
|
44
|
+
* because the public `/auth/developer/*` endpoints are NOT under `/admin`.
|
|
45
|
+
*/
|
|
46
|
+
getBaseUrl() {
|
|
47
|
+
return this.baseUrl;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns the API root with the trailing `/admin` segment stripped —
|
|
51
|
+
* computed once at construction so tools targeting sibling routes
|
|
52
|
+
* (`/auth/developer/*`) get a stable root regardless of trailing-slash
|
|
53
|
+
* normalisation. The constructor enforces the `/admin` suffix invariant.
|
|
54
|
+
*/
|
|
55
|
+
getApiRoot() {
|
|
56
|
+
return this.apiRoot;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the current developer Bearer token, going through the configured
|
|
60
|
+
* tokenProvider (or `~/.amba/credentials.json` for the CLI fallback).
|
|
61
|
+
*
|
|
62
|
+
* Returns `null` if no provider is configured AND no credentials file exists
|
|
63
|
+
* — useful for tools like `amba_developer_signup` that may run with no
|
|
64
|
+
* inbound Bearer at all. All other code paths should treat a thrown error
|
|
65
|
+
* as a hard auth failure (e.g. expired token).
|
|
66
|
+
*/
|
|
67
|
+
async resolveTokenOrNull() {
|
|
68
|
+
if (this.tokenProvider) try {
|
|
69
|
+
return await this.tokenProvider() || null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const credentials = await loadCredentials();
|
|
75
|
+
if (isTokenExpired(credentials)) return null;
|
|
76
|
+
return credentials.access_token;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async getToken() {
|
|
82
|
+
if (this.tokenProvider) {
|
|
83
|
+
const token = await this.tokenProvider();
|
|
84
|
+
if (!token) throw new Error("Token provider returned an empty token.");
|
|
85
|
+
return token;
|
|
86
|
+
}
|
|
87
|
+
const credentials = await loadCredentials();
|
|
88
|
+
if (isTokenExpired(credentials)) throw new Error("Developer access token has expired. Run \"amba login\" to re-authenticate.");
|
|
89
|
+
return credentials.access_token;
|
|
90
|
+
}
|
|
91
|
+
async request(method, path, body, query) {
|
|
92
|
+
const token = await this.getToken();
|
|
93
|
+
let url = `${this.baseUrl}${path}`;
|
|
94
|
+
if (query) {
|
|
95
|
+
const params = new URLSearchParams(query);
|
|
96
|
+
url += `?${params.toString()}`;
|
|
97
|
+
}
|
|
98
|
+
const headers = {
|
|
99
|
+
Authorization: `Bearer ${token}`,
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
Accept: "application/json"
|
|
102
|
+
};
|
|
103
|
+
const res = await fetch(url, {
|
|
104
|
+
method,
|
|
105
|
+
headers,
|
|
106
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
107
|
+
});
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
let errorMessage;
|
|
110
|
+
try {
|
|
111
|
+
errorMessage = (await res.json())?.error?.message ?? res.statusText;
|
|
112
|
+
} catch {
|
|
113
|
+
errorMessage = res.statusText;
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`API error ${res.status}: ${errorMessage}`);
|
|
116
|
+
}
|
|
117
|
+
return await res.json();
|
|
118
|
+
}
|
|
119
|
+
async get(path, query) {
|
|
120
|
+
return this.request("GET", path, void 0, query);
|
|
121
|
+
}
|
|
122
|
+
async post(path, body) {
|
|
123
|
+
return this.request("POST", path, body);
|
|
124
|
+
}
|
|
125
|
+
async put(path, body) {
|
|
126
|
+
return this.request("PUT", path, body);
|
|
127
|
+
}
|
|
128
|
+
async patch(path, body) {
|
|
129
|
+
return this.request("PATCH", path, body);
|
|
130
|
+
}
|
|
131
|
+
async delete(path) {
|
|
132
|
+
return this.request("DELETE", path);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* GET that returns the raw response body as a string and only reads up to
|
|
136
|
+
* `maxBytes` bytes. Used by export tools that hit CSV/NDJSON streaming
|
|
137
|
+
* endpoints — JSON parsing isn't applicable, and we cap at 10k rows worth
|
|
138
|
+
* of bytes so a multi-million-row export can't blow the MCP context window.
|
|
139
|
+
*
|
|
140
|
+
* Errors are still parsed via the JSON envelope where possible to keep the
|
|
141
|
+
* thrown message human-readable (matches the regular request() behavior).
|
|
142
|
+
*/
|
|
143
|
+
async getRaw(path, query, options) {
|
|
144
|
+
const token = await this.getToken();
|
|
145
|
+
const maxBytes = options?.maxBytes ?? 1048576;
|
|
146
|
+
let url = `${this.baseUrl}${path}`;
|
|
147
|
+
if (query) {
|
|
148
|
+
const params = new URLSearchParams(query);
|
|
149
|
+
url += `?${params.toString()}`;
|
|
150
|
+
}
|
|
151
|
+
const res = await fetch(url, {
|
|
152
|
+
method: "GET",
|
|
153
|
+
headers: {
|
|
154
|
+
Authorization: `Bearer ${token}`,
|
|
155
|
+
Accept: "text/csv, application/x-ndjson, */*"
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
let errorMessage;
|
|
160
|
+
try {
|
|
161
|
+
errorMessage = (await res.json())?.error?.message ?? res.statusText;
|
|
162
|
+
} catch {
|
|
163
|
+
errorMessage = res.statusText;
|
|
164
|
+
}
|
|
165
|
+
throw new Error(`API error ${res.status}: ${errorMessage}`);
|
|
166
|
+
}
|
|
167
|
+
const contentType = res.headers.get("content-type");
|
|
168
|
+
const reader = res.body?.getReader();
|
|
169
|
+
if (!reader) {
|
|
170
|
+
const full = await res.text();
|
|
171
|
+
const truncated = full.length > maxBytes;
|
|
172
|
+
return {
|
|
173
|
+
body: truncated ? full.slice(0, maxBytes) : full,
|
|
174
|
+
truncated,
|
|
175
|
+
contentType
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const decoder = new TextDecoder();
|
|
179
|
+
let received = 0;
|
|
180
|
+
let truncated = false;
|
|
181
|
+
let body = "";
|
|
182
|
+
while (received < maxBytes) {
|
|
183
|
+
const { done, value } = await reader.read();
|
|
184
|
+
if (done) break;
|
|
185
|
+
if (!value) continue;
|
|
186
|
+
const remaining = maxBytes - received;
|
|
187
|
+
if (value.byteLength > remaining) {
|
|
188
|
+
body += decoder.decode(value.slice(0, remaining), { stream: false });
|
|
189
|
+
received = maxBytes;
|
|
190
|
+
truncated = true;
|
|
191
|
+
await reader.cancel();
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
body += decoder.decode(value, { stream: true });
|
|
195
|
+
received += value.byteLength;
|
|
196
|
+
}
|
|
197
|
+
body += decoder.decode();
|
|
198
|
+
return {
|
|
199
|
+
body,
|
|
200
|
+
truncated,
|
|
201
|
+
contentType
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/tools/projects.ts
|
|
207
|
+
function registerTools$18(server, apiClient) {
|
|
208
|
+
server.tool("amba_list_projects", "List all Amba projects owned by the authenticated developer. Returns project id, name, bundle_id, platform, and environment.", {}, async () => {
|
|
209
|
+
const result = await apiClient.get("/projects");
|
|
210
|
+
return { content: [{
|
|
211
|
+
type: "text",
|
|
212
|
+
text: JSON.stringify(result, null, 2)
|
|
213
|
+
}] };
|
|
214
|
+
});
|
|
215
|
+
server.tool("amba_get_project", "Get detailed information about a specific Amba project by its ID, including API keys, integrations, and configuration.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
216
|
+
const result = await apiClient.get(`/projects/${project_id}`);
|
|
217
|
+
return { content: [{
|
|
218
|
+
type: "text",
|
|
219
|
+
text: JSON.stringify(result, null, 2)
|
|
220
|
+
}] };
|
|
221
|
+
});
|
|
222
|
+
server.tool("amba_create_project", "Create a new Amba project. A project represents a mobile app and contains all its engagement configuration (push, segments, config, content, streaks).", {
|
|
223
|
+
name: z.string().describe("Human-readable project name (e.g. \"My Fitness App\")"),
|
|
224
|
+
bundle_id: z.string().optional().describe("App bundle identifier (e.g. \"com.example.myapp\")"),
|
|
225
|
+
platform: z.enum([
|
|
226
|
+
"ios",
|
|
227
|
+
"android",
|
|
228
|
+
"all"
|
|
229
|
+
]).optional().describe("Target platform. Defaults to \"all\".")
|
|
230
|
+
}, async ({ name, bundle_id, platform }) => {
|
|
231
|
+
const body = { name };
|
|
232
|
+
if (bundle_id !== void 0) body.bundle_id = bundle_id;
|
|
233
|
+
if (platform !== void 0) body.platform = platform;
|
|
234
|
+
const result = await apiClient.post("/projects", body);
|
|
235
|
+
return { content: [{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: JSON.stringify(result, null, 2)
|
|
238
|
+
}] };
|
|
239
|
+
});
|
|
240
|
+
server.tool("amba_update_project", "Update a project. Only `name`, `bundle_id`, `platform`, and `environment` are mutable; other fields are silently ignored. At least one editable field must be supplied.", {
|
|
241
|
+
project_id: z.string().describe("The project ID"),
|
|
242
|
+
name: z.string().optional().describe("Human-readable project name."),
|
|
243
|
+
bundle_id: z.string().optional().describe("App bundle identifier (e.g. \"com.example.myapp\")."),
|
|
244
|
+
platform: z.enum([
|
|
245
|
+
"ios",
|
|
246
|
+
"android",
|
|
247
|
+
"all"
|
|
248
|
+
]).optional().describe("Target platform."),
|
|
249
|
+
environment: z.enum(["development", "production"]).optional().describe("Project environment.")
|
|
250
|
+
}, async ({ project_id, name, bundle_id, platform, environment }) => {
|
|
251
|
+
const payload = {};
|
|
252
|
+
if (name !== void 0) payload.name = name;
|
|
253
|
+
if (bundle_id !== void 0) payload.bundle_id = bundle_id;
|
|
254
|
+
if (platform !== void 0) payload.platform = platform;
|
|
255
|
+
if (environment !== void 0) payload.environment = environment;
|
|
256
|
+
const result = await apiClient.patch(`/projects/${project_id}`, payload);
|
|
257
|
+
return { content: [{
|
|
258
|
+
type: "text",
|
|
259
|
+
text: JSON.stringify(result, null, 2)
|
|
260
|
+
}] };
|
|
261
|
+
});
|
|
262
|
+
server.tool("amba_delete_project", "Permanently delete an Amba project. This removes the project from Amba metadata. The associated Neon database is NOT auto-deleted — clean it up via the Neon console if no longer needed.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
263
|
+
const result = await apiClient.delete(`/projects/${project_id}`);
|
|
264
|
+
return { content: [{
|
|
265
|
+
type: "text",
|
|
266
|
+
text: JSON.stringify(result, null, 2)
|
|
267
|
+
}] };
|
|
268
|
+
});
|
|
269
|
+
server.tool("amba_create_api_key", "Generate a new API key for a project. The full key value is returned ONCE in the response and cannot be retrieved later — store it immediately. Server keys are for backend services; client keys are for the Amba client SDKs (@layers/amba-web, @layers/amba-expo, @layers/amba-react-native, @layers/amba-node).", {
|
|
270
|
+
project_id: z.string().describe("The project ID"),
|
|
271
|
+
key_type: z.enum(["client", "server"]).describe("Key type. `client` for the SDK on end-user devices; `server` for backend services."),
|
|
272
|
+
environment: z.enum(["development", "production"]).describe("Environment this key targets.")
|
|
273
|
+
}, async ({ project_id, key_type, environment }) => {
|
|
274
|
+
const result = await apiClient.post(`/projects/${project_id}/api-keys`, {
|
|
275
|
+
key_type,
|
|
276
|
+
environment
|
|
277
|
+
});
|
|
278
|
+
return { content: [{
|
|
279
|
+
type: "text",
|
|
280
|
+
text: JSON.stringify(result, null, 2)
|
|
281
|
+
}] };
|
|
282
|
+
});
|
|
283
|
+
server.tool("amba_delete_api_key", "Revoke an API key. Subsequent requests using the key will be rejected with 401. Useful when a key is compromised or rotated.", {
|
|
284
|
+
project_id: z.string().describe("The project ID"),
|
|
285
|
+
key_id: z.string().describe("The API key ID to revoke")
|
|
286
|
+
}, async ({ project_id, key_id }) => {
|
|
287
|
+
const result = await apiClient.delete(`/projects/${project_id}/api-keys/${key_id}`);
|
|
288
|
+
return { content: [{
|
|
289
|
+
type: "text",
|
|
290
|
+
text: JSON.stringify(result, null, 2)
|
|
291
|
+
}] };
|
|
292
|
+
});
|
|
293
|
+
server.tool("amba_reprovision_project", "Re-drive a failed or stalled project provisioning. Kicks off the same provisioning workflow used at create-time but with a fresh per-attempt id, so duplicate POSTs collapse rather than fan out. Provisioning is idempotent.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
294
|
+
const result = await apiClient.post(`/projects/${project_id}/reprovision`);
|
|
295
|
+
return { content: [{
|
|
296
|
+
type: "text",
|
|
297
|
+
text: JSON.stringify(result, null, 2)
|
|
298
|
+
}] };
|
|
299
|
+
});
|
|
300
|
+
server.tool("amba_get_provisioning_status", "Inspect a project's Neon provisioning state. Returns the provisioning record plus a workflow descriptor (status, run id, start/close times) for the most recent provisioning attempt.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
301
|
+
const result = await apiClient.get(`/projects/${project_id}/provisioning-status`);
|
|
302
|
+
return { content: [{
|
|
303
|
+
type: "text",
|
|
304
|
+
text: JSON.stringify(result, null, 2)
|
|
305
|
+
}] };
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/tools/push.ts
|
|
310
|
+
function registerTools$17(server, apiClient) {
|
|
311
|
+
server.tool("amba_create_push_campaign", "Create a new push notification campaign for a project. The campaign starts in \"draft\" status. You can optionally target a segment and schedule delivery.", {
|
|
312
|
+
project_id: z.string().describe("The project ID"),
|
|
313
|
+
title: z.string().describe("Push notification title shown to the user"),
|
|
314
|
+
body: z.string().describe("Push notification body text"),
|
|
315
|
+
name: z.string().optional().describe("Internal campaign name for developer reference"),
|
|
316
|
+
segment_id: z.string().optional().describe("Target segment ID. If omitted, targets all users."),
|
|
317
|
+
data: z.record(z.unknown()).optional().describe("Custom key-value data payload attached to the push notification"),
|
|
318
|
+
scheduled_at: z.string().optional().describe("ISO 8601 datetime to schedule delivery. If omitted, the campaign remains a draft.")
|
|
319
|
+
}, async ({ project_id, title, body, name, segment_id, data, scheduled_at }) => {
|
|
320
|
+
const payload = {
|
|
321
|
+
title,
|
|
322
|
+
body
|
|
323
|
+
};
|
|
324
|
+
if (name !== void 0) payload.name = name;
|
|
325
|
+
if (segment_id !== void 0) payload.segment_id = segment_id;
|
|
326
|
+
if (data !== void 0) payload.data = data;
|
|
327
|
+
if (scheduled_at !== void 0) payload.scheduled_at = scheduled_at;
|
|
328
|
+
const result = await apiClient.post(`/projects/${project_id}/push/campaigns`, payload);
|
|
329
|
+
return { content: [{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: JSON.stringify(result, null, 2)
|
|
332
|
+
}] };
|
|
333
|
+
});
|
|
334
|
+
server.tool("amba_send_push_campaign", "Send (trigger delivery of) a previously created push campaign. The campaign must be in \"draft\" status. Scheduled campaigns fire automatically.", {
|
|
335
|
+
project_id: z.string().describe("The project ID"),
|
|
336
|
+
campaign_id: z.string().describe("The push campaign ID to send")
|
|
337
|
+
}, async ({ project_id, campaign_id }) => {
|
|
338
|
+
const result = await apiClient.post(`/projects/${project_id}/push/campaigns/${campaign_id}/send`);
|
|
339
|
+
return { content: [{
|
|
340
|
+
type: "text",
|
|
341
|
+
text: JSON.stringify(result, null, 2)
|
|
342
|
+
}] };
|
|
343
|
+
});
|
|
344
|
+
server.tool("amba_send_test_push", "Send a test push notification. Provide either `app_user_id` (uses the user's most recent active push token) or an explicit `device_token`+`provider` pair. Useful for verifying APNs/FCM configuration before launching a campaign.", {
|
|
345
|
+
project_id: z.string().describe("The project ID"),
|
|
346
|
+
title: z.string().describe("Push notification title"),
|
|
347
|
+
body: z.string().describe("Push notification body text"),
|
|
348
|
+
app_user_id: z.string().optional().describe("Target app user ID; server looks up their most recent active push token. Either this or device_token+provider is required."),
|
|
349
|
+
device_token: z.string().optional().describe("Explicit device push token (APNs or FCM). Requires `provider` when set."),
|
|
350
|
+
provider: z.enum(["apns", "fcm"]).optional().describe("Push provider when `device_token` is supplied. Required with device_token."),
|
|
351
|
+
data: z.record(z.unknown()).optional().describe("Optional custom data payload")
|
|
352
|
+
}, async ({ project_id, title, body, app_user_id, device_token, provider, data }) => {
|
|
353
|
+
const payload = {
|
|
354
|
+
title,
|
|
355
|
+
body
|
|
356
|
+
};
|
|
357
|
+
if (app_user_id !== void 0) payload.app_user_id = app_user_id;
|
|
358
|
+
if (device_token !== void 0) payload.device_token = device_token;
|
|
359
|
+
if (provider !== void 0) payload.provider = provider;
|
|
360
|
+
if (data !== void 0) payload.data = data;
|
|
361
|
+
const result = await apiClient.post(`/projects/${project_id}/push/campaigns/test`, payload);
|
|
362
|
+
return { content: [{
|
|
363
|
+
type: "text",
|
|
364
|
+
text: JSON.stringify(result, null, 2)
|
|
365
|
+
}] };
|
|
366
|
+
});
|
|
367
|
+
server.tool("amba_push_list_campaigns", "List push notification campaigns for a project, ordered by most recent. Returns name, title, body, segment_id, status, scheduled_at, and created_at.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
368
|
+
const result = await apiClient.get(`/projects/${project_id}/push/campaigns`);
|
|
369
|
+
return { content: [{
|
|
370
|
+
type: "text",
|
|
371
|
+
text: JSON.stringify(result, null, 2)
|
|
372
|
+
}] };
|
|
373
|
+
});
|
|
374
|
+
server.tool("amba_push_get_campaign", "Get a single push campaign by ID including its current status (draft, scheduled, sending, sent, failed), schedule, and payload.", {
|
|
375
|
+
project_id: z.string().describe("The project ID"),
|
|
376
|
+
campaign_id: z.string().describe("The push campaign ID")
|
|
377
|
+
}, async ({ project_id, campaign_id }) => {
|
|
378
|
+
const result = await apiClient.get(`/projects/${project_id}/push/campaigns/${campaign_id}`);
|
|
379
|
+
return { content: [{
|
|
380
|
+
type: "text",
|
|
381
|
+
text: JSON.stringify(result, null, 2)
|
|
382
|
+
}] };
|
|
383
|
+
});
|
|
384
|
+
server.tool("amba_push_update_campaign", "Update a push campaign before send. Only campaigns in `draft` or `scheduled` state are mutable; anything past that returns 409 ALREADY_SENT. If `scheduled_at` changes on a `scheduled` campaign, the existing Temporal workflow is terminated and re-started with the new startDelay. At least one editable field must be supplied.", {
|
|
385
|
+
project_id: z.string().describe("The project ID"),
|
|
386
|
+
campaign_id: z.string().describe("The push campaign ID"),
|
|
387
|
+
title: z.string().optional().describe("New notification title."),
|
|
388
|
+
body: z.string().optional().describe("New notification body text."),
|
|
389
|
+
data: z.record(z.unknown()).optional().describe("New custom data payload (object)."),
|
|
390
|
+
segment_id: z.string().nullable().optional().describe("New target segment UUID, or null to broadcast to all users."),
|
|
391
|
+
scheduled_at: z.string().optional().describe("New ISO-8601 send time. Must be in the future. Setting this on a draft promotes it to scheduled.")
|
|
392
|
+
}, async ({ project_id, campaign_id, title, body, data, segment_id, scheduled_at }) => {
|
|
393
|
+
const payload = {};
|
|
394
|
+
if (title !== void 0) payload.title = title;
|
|
395
|
+
if (body !== void 0) payload.body = body;
|
|
396
|
+
if (data !== void 0) payload.data = data;
|
|
397
|
+
if (segment_id !== void 0) payload.segment_id = segment_id;
|
|
398
|
+
if (scheduled_at !== void 0) payload.scheduled_at = scheduled_at;
|
|
399
|
+
const result = await apiClient.patch(`/projects/${project_id}/push/campaigns/${campaign_id}`, payload);
|
|
400
|
+
return { content: [{
|
|
401
|
+
type: "text",
|
|
402
|
+
text: JSON.stringify(result, null, 2)
|
|
403
|
+
}] };
|
|
404
|
+
});
|
|
405
|
+
server.tool("amba_push_delete_campaign", "Cancel and hard-delete a push campaign. Only campaigns in `draft` or `scheduled` state can be deleted; anything past that returns 409 ALREADY_SENT. Any pending Temporal workflow is terminated as part of the delete. Returns 204 No Content on success.", {
|
|
406
|
+
project_id: z.string().describe("The project ID"),
|
|
407
|
+
campaign_id: z.string().describe("The push campaign ID")
|
|
408
|
+
}, async ({ project_id, campaign_id }) => {
|
|
409
|
+
const result = await apiClient.delete(`/projects/${project_id}/push/campaigns/${campaign_id}`);
|
|
410
|
+
return { content: [{
|
|
411
|
+
type: "text",
|
|
412
|
+
text: JSON.stringify(result, null, 2)
|
|
413
|
+
}] };
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
//#endregion
|
|
417
|
+
//#region src/tools/segments.ts
|
|
418
|
+
const segmentConditionSchema = z.object({
|
|
419
|
+
field: z.string().describe("User property field to evaluate (e.g. \"last_seen_at\", \"properties.plan\", \"entitlements.is_active\")"),
|
|
420
|
+
op: z.enum([
|
|
421
|
+
"eq",
|
|
422
|
+
"neq",
|
|
423
|
+
"gt",
|
|
424
|
+
"gte",
|
|
425
|
+
"lt",
|
|
426
|
+
"lte",
|
|
427
|
+
"contains",
|
|
428
|
+
"not_contains",
|
|
429
|
+
"exists",
|
|
430
|
+
"not_exists",
|
|
431
|
+
"within",
|
|
432
|
+
"not_within"
|
|
433
|
+
]).describe("Comparison operator"),
|
|
434
|
+
value: z.unknown().describe("Value to compare against")
|
|
435
|
+
});
|
|
436
|
+
const segmentRulesSchema = z.object({
|
|
437
|
+
operator: z.enum(["AND", "OR"]).describe("Logical operator combining conditions"),
|
|
438
|
+
conditions: z.array(segmentConditionSchema).describe("Array of filter conditions")
|
|
439
|
+
});
|
|
440
|
+
function registerTools$16(server, apiClient) {
|
|
441
|
+
server.tool("amba_list_segments", "List all user segments for a project. Segments define groups of users based on rules (e.g. \"active in last 7 days\", \"premium users\"). System segments are included.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
442
|
+
const result = await apiClient.get(`/projects/${project_id}/segments`);
|
|
443
|
+
return { content: [{
|
|
444
|
+
type: "text",
|
|
445
|
+
text: JSON.stringify(result, null, 2)
|
|
446
|
+
}] };
|
|
447
|
+
});
|
|
448
|
+
server.tool("amba_create_segment", "Create a custom user segment with rule-based filters. Segments can target users by properties, activity, entitlements, and more. Used for push targeting and analytics.", {
|
|
449
|
+
project_id: z.string().describe("The project ID"),
|
|
450
|
+
name: z.string().describe("Segment name (e.g. \"Power Users\", \"Trial Expiring Soon\")"),
|
|
451
|
+
description: z.string().optional().describe("Human-readable description of the segment"),
|
|
452
|
+
rules: segmentRulesSchema.describe("Segment filter rules with conditions")
|
|
453
|
+
}, async ({ project_id, name, description, rules }) => {
|
|
454
|
+
const payload = {
|
|
455
|
+
name,
|
|
456
|
+
rules
|
|
457
|
+
};
|
|
458
|
+
if (description !== void 0) payload.description = description;
|
|
459
|
+
const result = await apiClient.post(`/projects/${project_id}/segments`, payload);
|
|
460
|
+
return { content: [{
|
|
461
|
+
type: "text",
|
|
462
|
+
text: JSON.stringify(result, null, 2)
|
|
463
|
+
}] };
|
|
464
|
+
});
|
|
465
|
+
server.tool("amba_segments_get", "Get a single segment by id including its rules and is_system flag.", {
|
|
466
|
+
project_id: z.string().describe("The project ID"),
|
|
467
|
+
segment_id: z.string().describe("The segment ID")
|
|
468
|
+
}, async ({ project_id, segment_id }) => {
|
|
469
|
+
const result = await apiClient.get(`/projects/${project_id}/segments/${segment_id}`);
|
|
470
|
+
return { content: [{
|
|
471
|
+
type: "text",
|
|
472
|
+
text: JSON.stringify(result, null, 2)
|
|
473
|
+
}] };
|
|
474
|
+
});
|
|
475
|
+
server.tool("amba_segments_patch", "Update a non-system segment's name, description, and/or rules. System segments cannot be edited (API returns 403).", {
|
|
476
|
+
project_id: z.string().describe("The project ID"),
|
|
477
|
+
segment_id: z.string().describe("The segment ID"),
|
|
478
|
+
name: z.string().optional().describe("New segment name"),
|
|
479
|
+
description: z.string().nullable().optional().describe("New description; pass null to clear"),
|
|
480
|
+
rules: segmentRulesSchema.optional().describe("Replacement rule tree")
|
|
481
|
+
}, async ({ project_id, segment_id, name, description, rules }) => {
|
|
482
|
+
const payload = {};
|
|
483
|
+
if (name !== void 0) payload.name = name;
|
|
484
|
+
if (description !== void 0) payload.description = description;
|
|
485
|
+
if (rules !== void 0) payload.rules = rules;
|
|
486
|
+
const result = await apiClient.patch(`/projects/${project_id}/segments/${segment_id}`, payload);
|
|
487
|
+
return { content: [{
|
|
488
|
+
type: "text",
|
|
489
|
+
text: JSON.stringify(result, null, 2)
|
|
490
|
+
}] };
|
|
491
|
+
});
|
|
492
|
+
server.tool("amba_segments_delete", "Delete a non-system segment. System segments cannot be deleted (API returns 403).", {
|
|
493
|
+
project_id: z.string().describe("The project ID"),
|
|
494
|
+
segment_id: z.string().describe("The segment ID")
|
|
495
|
+
}, async ({ project_id, segment_id }) => {
|
|
496
|
+
const result = await apiClient.delete(`/projects/${project_id}/segments/${segment_id}`);
|
|
497
|
+
return { content: [{
|
|
498
|
+
type: "text",
|
|
499
|
+
text: JSON.stringify(result, null, 2)
|
|
500
|
+
}] };
|
|
501
|
+
});
|
|
502
|
+
server.tool("amba_segments_evaluate", "Trigger an immediate segment evaluation workflow in Temporal. Useful after a rule change to refresh segment memberships without waiting for the 15-minute scheduled run.", {
|
|
503
|
+
project_id: z.string().describe("The project ID"),
|
|
504
|
+
segment_id: z.string().describe("The segment ID to evaluate")
|
|
505
|
+
}, async ({ project_id, segment_id }) => {
|
|
506
|
+
const result = await apiClient.post(`/projects/${project_id}/segments/${segment_id}/evaluate`);
|
|
507
|
+
return { content: [{
|
|
508
|
+
type: "text",
|
|
509
|
+
text: JSON.stringify(result, null, 2)
|
|
510
|
+
}] };
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/tools/config.ts
|
|
515
|
+
const configConditionSchema = z.object({
|
|
516
|
+
segment_id: z.string().optional().describe("Segment ID to conditionally apply this value"),
|
|
517
|
+
percentage: z.number().optional().describe("Percentage rollout (0-100)"),
|
|
518
|
+
value: z.unknown().describe("Override value for this condition")
|
|
519
|
+
});
|
|
520
|
+
function registerTools$15(server, apiClient) {
|
|
521
|
+
server.tool("amba_list_configs", "List all 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.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
522
|
+
const result = await apiClient.get(`/projects/${project_id}/config`);
|
|
523
|
+
return { content: [{
|
|
524
|
+
type: "text",
|
|
525
|
+
text: JSON.stringify(result, null, 2)
|
|
526
|
+
}] };
|
|
527
|
+
});
|
|
528
|
+
server.tool("amba_create_config", "Create or upsert a remote-config key. Supports string, number, boolean, and JSON value types. Can include segment-based or percentage-based conditions for targeted rollouts. The handler is upsert-on-key — calling this for an existing key replaces it.", {
|
|
529
|
+
project_id: z.string().describe("The project ID"),
|
|
530
|
+
key: z.string().describe("Config key name (e.g. \"feature_paywall_v2\", \"daily_limit\")"),
|
|
531
|
+
value: z.unknown().describe("The default config value"),
|
|
532
|
+
value_type: z.enum([
|
|
533
|
+
"string",
|
|
534
|
+
"number",
|
|
535
|
+
"boolean",
|
|
536
|
+
"json"
|
|
537
|
+
]).optional().describe("Value type. Defaults to \"string\"."),
|
|
538
|
+
description: z.string().optional().describe("Description of what this config controls"),
|
|
539
|
+
conditions: z.array(configConditionSchema).optional().describe("Conditional overrides based on segments or percentage rollout")
|
|
540
|
+
}, async ({ project_id, key, value, value_type, description, conditions }) => {
|
|
541
|
+
const payload = {
|
|
542
|
+
key,
|
|
543
|
+
value
|
|
544
|
+
};
|
|
545
|
+
if (value_type !== void 0) payload.value_type = value_type;
|
|
546
|
+
if (description !== void 0) payload.description = description;
|
|
547
|
+
if (conditions !== void 0) payload.conditions = conditions;
|
|
548
|
+
const result = await apiClient.post(`/projects/${project_id}/config`, payload);
|
|
549
|
+
return { content: [{
|
|
550
|
+
type: "text",
|
|
551
|
+
text: JSON.stringify(result, null, 2)
|
|
552
|
+
}] };
|
|
553
|
+
});
|
|
554
|
+
server.tool("amba_update_config", "Partially update a single remote-config key. Only fields provided are touched — other fields are preserved. Updating `value` bumps the per-row version counter; any successful mutation also recomputes the singleton config_versions hash so cached clients see the change.", {
|
|
555
|
+
project_id: z.string().describe("The project ID"),
|
|
556
|
+
key: z.string().describe("The config key to update"),
|
|
557
|
+
value: z.unknown().optional().describe("New default value (omit to leave unchanged)"),
|
|
558
|
+
description: z.string().nullable().optional().describe("New description (null to clear)"),
|
|
559
|
+
conditions: z.array(configConditionSchema).optional().describe("New conditional overrides (replaces the existing list)")
|
|
560
|
+
}, async ({ project_id, key, value, description, conditions }) => {
|
|
561
|
+
const payload = {};
|
|
562
|
+
if (value !== void 0) payload.value = value;
|
|
563
|
+
if (description !== void 0) payload.description = description;
|
|
564
|
+
if (conditions !== void 0) payload.conditions = conditions;
|
|
565
|
+
const result = await apiClient.patch(`/projects/${project_id}/config/${encodeURIComponent(key)}`, payload);
|
|
566
|
+
return { content: [{
|
|
567
|
+
type: "text",
|
|
568
|
+
text: JSON.stringify(result, null, 2)
|
|
569
|
+
}] };
|
|
570
|
+
});
|
|
571
|
+
server.tool("amba_delete_config", "Delete a remote-config key. Idempotent — deleting a non-existent key returns success. Recomputes the config_versions hash so clients refresh cached config on next fetch.", {
|
|
572
|
+
project_id: z.string().describe("The project ID"),
|
|
573
|
+
key: z.string().describe("The config key to delete")
|
|
574
|
+
}, async ({ project_id, key }) => {
|
|
575
|
+
const result = await apiClient.delete(`/projects/${project_id}/config/${encodeURIComponent(key)}`);
|
|
576
|
+
return { content: [{
|
|
577
|
+
type: "text",
|
|
578
|
+
text: JSON.stringify(result, null, 2)
|
|
579
|
+
}] };
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
//#endregion
|
|
583
|
+
//#region src/tools/content.ts
|
|
584
|
+
const contentItemSchema = z.object({
|
|
585
|
+
title: z.string().optional().describe("Content item title"),
|
|
586
|
+
body: z.string().describe("Content body text"),
|
|
587
|
+
media_url: z.string().optional().describe("URL to associated media (image, video)"),
|
|
588
|
+
category: z.string().optional().describe("Category for organizing content"),
|
|
589
|
+
tags: z.array(z.string()).optional().describe("Tags for content filtering"),
|
|
590
|
+
metadata: z.record(z.unknown()).optional().describe("Arbitrary metadata key-value pairs"),
|
|
591
|
+
is_premium: z.boolean().optional().describe("Whether this content requires a premium entitlement")
|
|
592
|
+
});
|
|
593
|
+
function registerTools$14(server, apiClient) {
|
|
594
|
+
server.tool("amba_create_content_library", "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.", {
|
|
595
|
+
project_id: z.string().describe("The project ID"),
|
|
596
|
+
name: z.string().describe("Library name (e.g. \"Daily Motivation\", \"Workout Tips\")"),
|
|
597
|
+
description: z.string().optional().describe("Description of the library content"),
|
|
598
|
+
content_schema: z.record(z.unknown()).optional().describe("Optional JSON schema for validating content item metadata")
|
|
599
|
+
}, async ({ project_id, name, description, content_schema }) => {
|
|
600
|
+
const payload = { name };
|
|
601
|
+
if (description !== void 0) payload.description = description;
|
|
602
|
+
if (content_schema !== void 0) payload.content_schema = content_schema;
|
|
603
|
+
const result = await apiClient.post(`/projects/${project_id}/content/libraries`, payload);
|
|
604
|
+
return { content: [{
|
|
605
|
+
type: "text",
|
|
606
|
+
text: JSON.stringify(result, null, 2)
|
|
607
|
+
}] };
|
|
608
|
+
});
|
|
609
|
+
server.tool("amba_add_content_items", "Add one or more content items to an existing content library. Items can include text, media URLs, categories, tags, and metadata.", {
|
|
610
|
+
project_id: z.string().describe("The project ID"),
|
|
611
|
+
library_id: z.string().describe("The content library ID"),
|
|
612
|
+
items: z.array(contentItemSchema).describe("Array of content items to add")
|
|
613
|
+
}, async ({ project_id, library_id, items }) => {
|
|
614
|
+
const result = await apiClient.post(`/projects/${project_id}/content/libraries/${library_id}/items`, { items });
|
|
615
|
+
return { content: [{
|
|
616
|
+
type: "text",
|
|
617
|
+
text: JSON.stringify(result, null, 2)
|
|
618
|
+
}] };
|
|
619
|
+
});
|
|
620
|
+
server.tool("amba_create_content_schedule", "Create a delivery schedule for a content library. Schedules control how and when content items are delivered to users (daily rotation, weekly, random, or sequential).", {
|
|
621
|
+
project_id: z.string().describe("The project ID"),
|
|
622
|
+
library_id: z.string().describe("The content library ID to schedule"),
|
|
623
|
+
name: z.string().describe("Schedule name (e.g. \"Morning Tip of the Day\")"),
|
|
624
|
+
schedule_type: z.enum([
|
|
625
|
+
"daily_rotation",
|
|
626
|
+
"weekly",
|
|
627
|
+
"random",
|
|
628
|
+
"sequential"
|
|
629
|
+
]).describe("How content items are selected for delivery"),
|
|
630
|
+
config: z.record(z.unknown()).optional().describe("Schedule-specific configuration (e.g. delivery time, timezone)")
|
|
631
|
+
}, async ({ project_id, library_id, name, schedule_type, config }) => {
|
|
632
|
+
const payload = {
|
|
633
|
+
library_id,
|
|
634
|
+
name,
|
|
635
|
+
schedule_type
|
|
636
|
+
};
|
|
637
|
+
if (config !== void 0) payload.config = config;
|
|
638
|
+
const result = await apiClient.post(`/projects/${project_id}/content/schedules`, payload);
|
|
639
|
+
return { content: [{
|
|
640
|
+
type: "text",
|
|
641
|
+
text: JSON.stringify(result, null, 2)
|
|
642
|
+
}] };
|
|
643
|
+
});
|
|
644
|
+
server.tool("amba_content_list_libraries", "List content libraries for a project.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
645
|
+
const result = await apiClient.get(`/projects/${project_id}/content/libraries`);
|
|
646
|
+
return { content: [{
|
|
647
|
+
type: "text",
|
|
648
|
+
text: JSON.stringify(result, null, 2)
|
|
649
|
+
}] };
|
|
650
|
+
});
|
|
651
|
+
server.tool("amba_content_list_items", "List content items in a specific library. Supports pagination.", {
|
|
652
|
+
project_id: z.string().describe("The project ID"),
|
|
653
|
+
library_id: z.string().describe("The content library ID"),
|
|
654
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
655
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
656
|
+
}, async ({ project_id, library_id, limit, offset }) => {
|
|
657
|
+
const query = {};
|
|
658
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
659
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
660
|
+
const result = await apiClient.get(`/projects/${project_id}/content/libraries/${library_id}/items`, query);
|
|
661
|
+
return { content: [{
|
|
662
|
+
type: "text",
|
|
663
|
+
text: JSON.stringify(result, null, 2)
|
|
664
|
+
}] };
|
|
665
|
+
});
|
|
666
|
+
server.tool("amba_content_list_schedules", "List all content delivery schedules for a project.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
667
|
+
const result = await apiClient.get(`/projects/${project_id}/content/schedules`);
|
|
668
|
+
return { content: [{
|
|
669
|
+
type: "text",
|
|
670
|
+
text: JSON.stringify(result, null, 2)
|
|
671
|
+
}] };
|
|
672
|
+
});
|
|
673
|
+
server.tool("amba_content_delete_item", "Delete a single content item permanently. Historical deliveries referencing this item are preserved.", {
|
|
674
|
+
project_id: z.string().describe("The project ID"),
|
|
675
|
+
item_id: z.string().describe("The content item ID")
|
|
676
|
+
}, async ({ project_id, item_id }) => {
|
|
677
|
+
const result = await apiClient.delete(`/projects/${project_id}/content/items/${item_id}`);
|
|
678
|
+
return { content: [{
|
|
679
|
+
type: "text",
|
|
680
|
+
text: JSON.stringify(result, null, 2)
|
|
681
|
+
}] };
|
|
682
|
+
});
|
|
683
|
+
server.tool("amba_content_update_item", "Partially update a content item. Only the supplied fields are touched. Use this to edit body, title, metadata, or active state without recreating the row.", {
|
|
684
|
+
project_id: z.string().describe("The project ID"),
|
|
685
|
+
item_id: z.string().describe("The content item ID"),
|
|
686
|
+
title: z.string().optional().describe("Item title"),
|
|
687
|
+
body: z.string().optional().describe("Item body / payload text"),
|
|
688
|
+
metadata: z.record(z.unknown()).optional().describe("Custom metadata"),
|
|
689
|
+
tags: z.array(z.string()).optional().describe("Tag list (replaces existing tags)"),
|
|
690
|
+
is_active: z.boolean().optional().describe("Whether the item is active")
|
|
691
|
+
}, async ({ project_id, item_id, title, body, metadata, tags, is_active }) => {
|
|
692
|
+
const payload = {};
|
|
693
|
+
if (title !== void 0) payload.title = title;
|
|
694
|
+
if (body !== void 0) payload.body = body;
|
|
695
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
696
|
+
if (tags !== void 0) payload.tags = tags;
|
|
697
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
698
|
+
const result = await apiClient.patch(`/projects/${project_id}/content/items/${item_id}`, payload);
|
|
699
|
+
return { content: [{
|
|
700
|
+
type: "text",
|
|
701
|
+
text: JSON.stringify(result, null, 2)
|
|
702
|
+
}] };
|
|
703
|
+
});
|
|
704
|
+
server.tool("amba_content_update_schedule", "Partially update a content delivery schedule. Only the supplied fields are touched. Updating timing or targeting fields restarts the underlying Temporal cron schedule.", {
|
|
705
|
+
project_id: z.string().describe("The project ID"),
|
|
706
|
+
schedule_id: z.string().describe("The schedule ID"),
|
|
707
|
+
name: z.string().optional().describe("Schedule name"),
|
|
708
|
+
cron: z.string().optional().describe("Cron expression (5-field) for delivery timing"),
|
|
709
|
+
timezone: z.string().optional().describe("IANA timezone (e.g. \"America/Los_Angeles\")"),
|
|
710
|
+
segment_id: z.string().nullable().optional().describe("Target segment, or null to broadcast to all users"),
|
|
711
|
+
is_active: z.boolean().optional().describe("Whether the schedule is active")
|
|
712
|
+
}, async ({ project_id, schedule_id, name, cron, timezone, segment_id, is_active }) => {
|
|
713
|
+
const payload = {};
|
|
714
|
+
if (name !== void 0) payload.name = name;
|
|
715
|
+
if (cron !== void 0) payload.cron = cron;
|
|
716
|
+
if (timezone !== void 0) payload.timezone = timezone;
|
|
717
|
+
if (segment_id !== void 0) payload.segment_id = segment_id;
|
|
718
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
719
|
+
const result = await apiClient.patch(`/projects/${project_id}/content/schedules/${schedule_id}`, payload);
|
|
720
|
+
return { content: [{
|
|
721
|
+
type: "text",
|
|
722
|
+
text: JSON.stringify(result, null, 2)
|
|
723
|
+
}] };
|
|
724
|
+
});
|
|
725
|
+
server.tool("amba_content_delete_schedule", "Delete a content delivery schedule. The underlying Temporal cron is terminated as part of the delete.", {
|
|
726
|
+
project_id: z.string().describe("The project ID"),
|
|
727
|
+
schedule_id: z.string().describe("The schedule ID")
|
|
728
|
+
}, async ({ project_id, schedule_id }) => {
|
|
729
|
+
const result = await apiClient.delete(`/projects/${project_id}/content/schedules/${schedule_id}`);
|
|
730
|
+
return { content: [{
|
|
731
|
+
type: "text",
|
|
732
|
+
text: JSON.stringify(result, null, 2)
|
|
733
|
+
}] };
|
|
734
|
+
});
|
|
735
|
+
server.tool("amba_content_bulk_import", "Bulk-import multiple content items into a library in a single transactional call. Each item is upsert-on-key within the library. Useful for seed data and for migrating from external content sources.", {
|
|
736
|
+
project_id: z.string().describe("The project ID"),
|
|
737
|
+
library_id: z.string().describe("The content library ID"),
|
|
738
|
+
items: z.array(z.record(z.unknown())).describe("Array of content items to import. Each item should include `key`, `title`, `body`, and optional `metadata`, `tags`.")
|
|
739
|
+
}, async ({ project_id, library_id, items }) => {
|
|
740
|
+
const result = await apiClient.post(`/projects/${project_id}/content/libraries/${library_id}/bulk`, { items });
|
|
741
|
+
return { content: [{
|
|
742
|
+
type: "text",
|
|
743
|
+
text: JSON.stringify(result, null, 2)
|
|
744
|
+
}] };
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/tools/streaks.ts
|
|
749
|
+
function registerTools$13(server, apiClient) {
|
|
750
|
+
server.tool("amba_create_streak", "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.", {
|
|
751
|
+
project_id: z.string().describe("The project ID"),
|
|
752
|
+
name: z.string().describe("Streak name (e.g. \"Daily Login\", \"Workout Streak\")"),
|
|
753
|
+
description: z.string().optional().describe("Description of what this streak tracks"),
|
|
754
|
+
qualifying_event: z.string().describe("The event name that qualifies for the streak (e.g. \"app_open\", \"workout_completed\")"),
|
|
755
|
+
period: z.enum(["daily", "weekly"]).optional().describe("Streak period. Defaults to \"daily\"."),
|
|
756
|
+
grace_period_hours: z.number().optional().describe("Hours of grace before a streak breaks (default 0)"),
|
|
757
|
+
freeze_enabled: z.boolean().optional().describe("Whether users can freeze their streak to prevent breaking it"),
|
|
758
|
+
max_freezes: z.number().optional().describe("Maximum number of streak freezes (shields) a user can hold concurrently"),
|
|
759
|
+
freezes_per_n_events: z.number().nullable().optional().describe("Auto-grant rule: every N consecutive qualifying events grants 1 freeze (shield), capped at max_freezes. Pass null or omit to disable auto-grant.")
|
|
760
|
+
}, async ({ project_id, name, description, qualifying_event, period, grace_period_hours, freeze_enabled, max_freezes, freezes_per_n_events }) => {
|
|
761
|
+
const payload = {
|
|
762
|
+
name,
|
|
763
|
+
qualifying_event
|
|
764
|
+
};
|
|
765
|
+
if (description !== void 0) payload.description = description;
|
|
766
|
+
if (period !== void 0) payload.period = period;
|
|
767
|
+
if (grace_period_hours !== void 0) payload.grace_period_hours = grace_period_hours;
|
|
768
|
+
if (freeze_enabled !== void 0) payload.freeze_enabled = freeze_enabled;
|
|
769
|
+
if (max_freezes !== void 0) payload.max_freezes = max_freezes;
|
|
770
|
+
if (freezes_per_n_events !== void 0) payload.freezes_per_n_events = freezes_per_n_events;
|
|
771
|
+
const result = await apiClient.post(`/projects/${project_id}/streaks`, payload);
|
|
772
|
+
return { content: [{
|
|
773
|
+
type: "text",
|
|
774
|
+
text: JSON.stringify(result, null, 2)
|
|
775
|
+
}] };
|
|
776
|
+
});
|
|
777
|
+
server.tool("amba_streaks_list", "List all streak definitions for a project, ordered by name.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
778
|
+
const result = await apiClient.get(`/projects/${project_id}/streaks`);
|
|
779
|
+
return { content: [{
|
|
780
|
+
type: "text",
|
|
781
|
+
text: JSON.stringify(result, null, 2)
|
|
782
|
+
}] };
|
|
783
|
+
});
|
|
784
|
+
server.tool("amba_streaks_update", "Update a streak definition. Only the supplied fields are changed; omitted fields are left untouched.", {
|
|
785
|
+
project_id: z.string().describe("The project ID"),
|
|
786
|
+
streak_id: z.string().describe("The streak definition ID"),
|
|
787
|
+
name: z.string().optional().describe("New streak name"),
|
|
788
|
+
description: z.string().optional().describe("New description"),
|
|
789
|
+
qualifying_event: z.string().optional().describe("New qualifying event name"),
|
|
790
|
+
period: z.enum(["daily", "weekly"]).optional().describe("New streak period"),
|
|
791
|
+
grace_period_hours: z.number().optional().describe("New grace period in hours"),
|
|
792
|
+
freeze_enabled: z.boolean().optional().describe("Toggle freeze support"),
|
|
793
|
+
max_freezes: z.number().optional().describe("New shield cap"),
|
|
794
|
+
freezes_per_n_events: z.number().nullable().optional().describe("Auto-grant rule. Pass null to disable; pass a positive integer to grant 1 shield every N qualifying events (capped at max_freezes).")
|
|
795
|
+
}, async ({ project_id, streak_id, name, description, qualifying_event, period, grace_period_hours, freeze_enabled, max_freezes, freezes_per_n_events }) => {
|
|
796
|
+
const payload = {};
|
|
797
|
+
if (name !== void 0) payload.name = name;
|
|
798
|
+
if (description !== void 0) payload.description = description;
|
|
799
|
+
if (qualifying_event !== void 0) payload.qualifying_event = qualifying_event;
|
|
800
|
+
if (period !== void 0) payload.period = period;
|
|
801
|
+
if (grace_period_hours !== void 0) payload.grace_period_hours = grace_period_hours;
|
|
802
|
+
if (freeze_enabled !== void 0) payload.freeze_enabled = freeze_enabled;
|
|
803
|
+
if (max_freezes !== void 0) payload.max_freezes = max_freezes;
|
|
804
|
+
if (freezes_per_n_events !== void 0) payload.freezes_per_n_events = freezes_per_n_events;
|
|
805
|
+
const result = await apiClient.patch(`/projects/${project_id}/streaks/${streak_id}`, payload);
|
|
806
|
+
return { content: [{
|
|
807
|
+
type: "text",
|
|
808
|
+
text: JSON.stringify(result, null, 2)
|
|
809
|
+
}] };
|
|
810
|
+
});
|
|
811
|
+
server.tool("amba_streaks_delete", "Delete a streak definition. Existing user_streak rows are removed by DB cascade; historical streak_events may be preserved depending on schema.", {
|
|
812
|
+
project_id: z.string().describe("The project ID"),
|
|
813
|
+
streak_id: z.string().describe("The streak definition ID")
|
|
814
|
+
}, async ({ project_id, streak_id }) => {
|
|
815
|
+
const result = await apiClient.delete(`/projects/${project_id}/streaks/${streak_id}`);
|
|
816
|
+
return { content: [{
|
|
817
|
+
type: "text",
|
|
818
|
+
text: JSON.stringify(result, null, 2)
|
|
819
|
+
}] };
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
//#endregion
|
|
823
|
+
//#region src/tools/integrations.ts
|
|
824
|
+
const providerEnum = z.enum(INTEGRATION_PROVIDERS);
|
|
825
|
+
function registerTools$12(server, apiClient) {
|
|
826
|
+
server.tool("amba_configure_integration", "Configure (create or replace) a third-party integration for a project. Supported providers: \"apns\" (Apple Push), \"fcm\" (Firebase Cloud Messaging), \"revenuecat\" (subscription management), \"superwall\" (paywall management). Each provider requires specific config fields.", {
|
|
827
|
+
project_id: z.string().describe("The project ID"),
|
|
828
|
+
provider: providerEnum.describe("Integration provider name"),
|
|
829
|
+
config: z.record(z.unknown()).describe("Provider-specific configuration. For APNs: { key_id, team_id, bundle_id, key_p8 }. For FCM: { service_account_json }. For RevenueCat: { api_key, webhook_secret }. For Superwall: { api_key }.")
|
|
830
|
+
}, async ({ project_id, provider, config }) => {
|
|
831
|
+
const result = await apiClient.post(`/projects/${project_id}/integrations`, {
|
|
832
|
+
provider,
|
|
833
|
+
config
|
|
834
|
+
});
|
|
835
|
+
return { content: [{
|
|
836
|
+
type: "text",
|
|
837
|
+
text: JSON.stringify(result, null, 2)
|
|
838
|
+
}] };
|
|
839
|
+
});
|
|
840
|
+
server.tool("amba_test_integration", "Test an existing integration to verify it is configured correctly and can connect to the third-party service. For APNs/FCM this calls the provider with a deliberately-invalid token to prove the credentials themselves are valid.", {
|
|
841
|
+
project_id: z.string().describe("The project ID"),
|
|
842
|
+
provider: providerEnum.describe("Integration provider to test")
|
|
843
|
+
}, async ({ project_id, provider }) => {
|
|
844
|
+
const result = await apiClient.post(`/projects/${project_id}/integrations/${provider}/test`);
|
|
845
|
+
return { content: [{
|
|
846
|
+
type: "text",
|
|
847
|
+
text: JSON.stringify(result, null, 2)
|
|
848
|
+
}] };
|
|
849
|
+
});
|
|
850
|
+
server.tool("amba_integrations_list", "List all integrations configured for a project. Returns provider, is_active, and timestamps. Secrets are not returned.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
851
|
+
const result = await apiClient.get(`/projects/${project_id}/integrations`);
|
|
852
|
+
return { content: [{
|
|
853
|
+
type: "text",
|
|
854
|
+
text: JSON.stringify(result, null, 2)
|
|
855
|
+
}] };
|
|
856
|
+
});
|
|
857
|
+
server.tool("amba_integrations_set", "Create or replace an integration (upsert by project+provider). This is an alias for `amba_configure_integration` that matches the audit naming scheme.", {
|
|
858
|
+
project_id: z.string().describe("The project ID"),
|
|
859
|
+
provider: providerEnum.describe("Integration provider name"),
|
|
860
|
+
config: z.record(z.unknown()).describe("Provider-specific configuration (see amba_configure_integration)")
|
|
861
|
+
}, async ({ project_id, provider, config }) => {
|
|
862
|
+
const result = await apiClient.post(`/projects/${project_id}/integrations`, {
|
|
863
|
+
provider,
|
|
864
|
+
config
|
|
865
|
+
});
|
|
866
|
+
return { content: [{
|
|
867
|
+
type: "text",
|
|
868
|
+
text: JSON.stringify(result, null, 2)
|
|
869
|
+
}] };
|
|
870
|
+
});
|
|
871
|
+
server.tool("amba_integrations_patch", "Patch an existing integration — update its config and/or toggle is_active. Omitted fields are left unchanged.", {
|
|
872
|
+
project_id: z.string().describe("The project ID"),
|
|
873
|
+
provider: providerEnum.describe("Integration provider name"),
|
|
874
|
+
config: z.record(z.unknown()).optional().describe("Replacement provider-specific configuration"),
|
|
875
|
+
is_active: z.boolean().optional().describe("Toggle whether the integration is active")
|
|
876
|
+
}, async ({ project_id, provider, config, is_active }) => {
|
|
877
|
+
const payload = {};
|
|
878
|
+
if (config !== void 0) payload.config = config;
|
|
879
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
880
|
+
const result = await apiClient.patch(`/projects/${project_id}/integrations/${provider}`, payload);
|
|
881
|
+
return { content: [{
|
|
882
|
+
type: "text",
|
|
883
|
+
text: JSON.stringify(result, null, 2)
|
|
884
|
+
}] };
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region src/tools/analytics.ts
|
|
889
|
+
function registerTools$11(server, apiClient) {
|
|
890
|
+
server.tool("amba_get_analytics", "Get analytics overview for a project. Returns DAU, MAU, new users, retention (D1/D7), total events, active streaks, push stats, and premium/free user counts for the specified period.", {
|
|
891
|
+
project_id: z.string().describe("The project ID"),
|
|
892
|
+
period: z.enum([
|
|
893
|
+
"24h",
|
|
894
|
+
"7d",
|
|
895
|
+
"30d",
|
|
896
|
+
"90d"
|
|
897
|
+
]).optional().describe("Time period for analytics. Defaults to \"7d\".")
|
|
898
|
+
}, async ({ project_id, period }) => {
|
|
899
|
+
const query = {};
|
|
900
|
+
if (period !== void 0) query.period = period;
|
|
901
|
+
const result = await apiClient.get(`/projects/${project_id}/analytics`, query);
|
|
902
|
+
return { content: [{
|
|
903
|
+
type: "text",
|
|
904
|
+
text: JSON.stringify(result, null, 2)
|
|
905
|
+
}] };
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region src/tools/users.ts
|
|
910
|
+
function registerTools$10(server, apiClient) {
|
|
911
|
+
server.tool("amba_list_users", "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.", {
|
|
912
|
+
project_id: z.string().describe("The project ID"),
|
|
913
|
+
limit: z.number().optional().describe("Maximum number of users to return (default 50)"),
|
|
914
|
+
offset: z.number().optional().describe("Number of users to skip for pagination (default 0)"),
|
|
915
|
+
segment_id: z.string().optional().describe("Filter users by segment ID"),
|
|
916
|
+
search: z.string().optional().describe("Free-text search across email, external_id, and display_name (case-insensitive)")
|
|
917
|
+
}, async ({ project_id, limit, offset, segment_id, search }) => {
|
|
918
|
+
const query = {};
|
|
919
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
920
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
921
|
+
if (segment_id !== void 0) query.segment_id = segment_id;
|
|
922
|
+
if (search !== void 0) query.search = search;
|
|
923
|
+
const result = await apiClient.get(`/projects/${project_id}/users`, query);
|
|
924
|
+
return { content: [{
|
|
925
|
+
type: "text",
|
|
926
|
+
text: JSON.stringify(result, null, 2)
|
|
927
|
+
}] };
|
|
928
|
+
});
|
|
929
|
+
server.tool("amba_users_get", "Get a single app user by id, with their streaks and entitlements joined in.", {
|
|
930
|
+
project_id: z.string().describe("The project ID"),
|
|
931
|
+
user_id: z.string().describe("The app user ID")
|
|
932
|
+
}, async ({ project_id, user_id }) => {
|
|
933
|
+
const result = await apiClient.get(`/projects/${project_id}/users/${user_id}`);
|
|
934
|
+
return { content: [{
|
|
935
|
+
type: "text",
|
|
936
|
+
text: JSON.stringify(result, null, 2)
|
|
937
|
+
}] };
|
|
938
|
+
});
|
|
939
|
+
server.tool("amba_users_list_events", "List a single user's engagement events, newest first. Useful for debugging tracking issues or building a user timeline.", {
|
|
940
|
+
project_id: z.string().describe("The project ID"),
|
|
941
|
+
user_id: z.string().describe("The app user ID"),
|
|
942
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
943
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
944
|
+
}, async ({ project_id, user_id, limit, offset }) => {
|
|
945
|
+
const query = {};
|
|
946
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
947
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
948
|
+
const result = await apiClient.get(`/projects/${project_id}/users/${user_id}/events`, query);
|
|
949
|
+
return { content: [{
|
|
950
|
+
type: "text",
|
|
951
|
+
text: JSON.stringify(result, null, 2)
|
|
952
|
+
}] };
|
|
953
|
+
});
|
|
954
|
+
server.tool("amba_users_export", "Export all app users for a project as CSV (default) or NDJSON. Streams via DB cursor on the API side. The MCP tool reads back at most ~1 MiB of body — for very large tenants prefer `format: \"ndjson\"` (parsable line-by-line) and use a `since` filter to scope the export. The returned `truncated` flag is true if the body was clipped to fit the MCP context budget.", {
|
|
955
|
+
project_id: z.string().describe("The project ID"),
|
|
956
|
+
format: z.enum(["csv", "ndjson"]).optional().describe("Response format. Default csv. Recommend ndjson for parsability."),
|
|
957
|
+
since: z.string().optional().describe("ISO-8601 lower bound on `created_at`. Useful for incremental exports.")
|
|
958
|
+
}, async ({ project_id, format, since }) => {
|
|
959
|
+
const query = {};
|
|
960
|
+
if (format !== void 0) query.format = format;
|
|
961
|
+
if (since !== void 0) query.since = since;
|
|
962
|
+
const { body, truncated, contentType } = await apiClient.getRaw(`/projects/${project_id}/users/export`, query);
|
|
963
|
+
return { content: [{
|
|
964
|
+
type: "text",
|
|
965
|
+
text: `# format=${format ?? "csv"} truncated=${truncated} content-type=${contentType ?? "unknown"}\n` + body
|
|
966
|
+
}] };
|
|
967
|
+
});
|
|
968
|
+
server.tool("amba_users_events_export", "Export one user's engagement events as CSV (default) or NDJSON. Streams via DB cursor on the API side. Filters: `since`, `until`, `event_name`. The MCP tool reads back at most ~1 MiB; the returned `truncated` flag indicates whether the body was clipped.", {
|
|
969
|
+
project_id: z.string().describe("The project ID"),
|
|
970
|
+
user_id: z.string().describe("The app user ID"),
|
|
971
|
+
format: z.enum(["csv", "ndjson"]).optional().describe("Response format. Default csv. Recommend ndjson for parsability."),
|
|
972
|
+
since: z.string().optional().describe("ISO-8601 lower bound on `occurred_at`."),
|
|
973
|
+
until: z.string().optional().describe("ISO-8601 upper bound on `occurred_at`."),
|
|
974
|
+
event_name: z.string().optional().describe("Filter to a specific event_name.")
|
|
975
|
+
}, async ({ project_id, user_id, format, since, until, event_name }) => {
|
|
976
|
+
const query = {};
|
|
977
|
+
if (format !== void 0) query.format = format;
|
|
978
|
+
if (since !== void 0) query.since = since;
|
|
979
|
+
if (until !== void 0) query.until = until;
|
|
980
|
+
if (event_name !== void 0) query.event_name = event_name;
|
|
981
|
+
const { body, truncated, contentType } = await apiClient.getRaw(`/projects/${project_id}/users/${user_id}/events/export`, query);
|
|
982
|
+
return { content: [{
|
|
983
|
+
type: "text",
|
|
984
|
+
text: `# format=${format ?? "csv"} truncated=${truncated} content-type=${contentType ?? "unknown"}\n` + body
|
|
985
|
+
}] };
|
|
986
|
+
});
|
|
987
|
+
server.tool("amba_users_bulk_update", "Apply property merges and/or segment add/remove to up to 1000 users in a single transactional call. Property updates use jsonb concat (top-level merge). Returns `{updated, failed}` where `failed` lists user_ids that did not exist in the project. At least one of `properties`, `add_segment_ids`, or `remove_segment_ids` must be set.", {
|
|
988
|
+
project_id: z.string().describe("The project ID"),
|
|
989
|
+
user_ids: z.array(z.string()).min(1).max(1e3).describe("Array of app_user UUIDs (1-1000). Larger batches return BATCH_TOO_LARGE."),
|
|
990
|
+
updates: z.object({
|
|
991
|
+
properties: z.record(z.unknown()).optional().describe("Top-level keys merged into each user's jsonb properties."),
|
|
992
|
+
add_segment_ids: z.array(z.string()).optional().describe("Segment UUIDs to add membership for (idempotent)."),
|
|
993
|
+
remove_segment_ids: z.array(z.string()).optional().describe("Segment UUIDs to remove membership from.")
|
|
994
|
+
}).describe("Update payload. At least one field must be present.")
|
|
995
|
+
}, async ({ project_id, user_ids, updates }) => {
|
|
996
|
+
const result = await apiClient.post(`/projects/${project_id}/users/bulk-update`, {
|
|
997
|
+
user_ids,
|
|
998
|
+
updates
|
|
999
|
+
});
|
|
1000
|
+
return { content: [{
|
|
1001
|
+
type: "text",
|
|
1002
|
+
text: JSON.stringify(result, null, 2)
|
|
1003
|
+
}] };
|
|
1004
|
+
});
|
|
1005
|
+
server.tool("amba_sessions_analytics", "Get session analytics (DAU, active users, total sessions, average and median session duration) for a project across a rolling window.", {
|
|
1006
|
+
project_id: z.string().describe("The project ID"),
|
|
1007
|
+
period: z.enum([
|
|
1008
|
+
"24h",
|
|
1009
|
+
"7d",
|
|
1010
|
+
"30d",
|
|
1011
|
+
"90d"
|
|
1012
|
+
]).optional().describe("Rolling window for the analytics. Defaults to 7d.")
|
|
1013
|
+
}, async ({ project_id, period }) => {
|
|
1014
|
+
const query = {};
|
|
1015
|
+
if (period) query.period = period;
|
|
1016
|
+
const result = await apiClient.get(`/projects/${project_id}/sessions`, query);
|
|
1017
|
+
return { content: [{
|
|
1018
|
+
type: "text",
|
|
1019
|
+
text: JSON.stringify(result, null, 2)
|
|
1020
|
+
}] };
|
|
1021
|
+
});
|
|
1022
|
+
server.tool("amba_sessions_list", "List recent app sessions for a project, newest first. Supports pagination.", {
|
|
1023
|
+
project_id: z.string().describe("The project ID"),
|
|
1024
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
1025
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
1026
|
+
}, async ({ project_id, limit, offset }) => {
|
|
1027
|
+
const query = {};
|
|
1028
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
1029
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
1030
|
+
const result = await apiClient.get(`/projects/${project_id}/sessions/list`, query);
|
|
1031
|
+
return { content: [{
|
|
1032
|
+
type: "text",
|
|
1033
|
+
text: JSON.stringify(result, null, 2)
|
|
1034
|
+
}] };
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/tools/setup.ts
|
|
1039
|
+
const SDK_SNIPPETS = {
|
|
1040
|
+
ios: `// Swift — AppDelegate.swift or App init
|
|
1041
|
+
import Amba
|
|
1042
|
+
|
|
1043
|
+
@main
|
|
1044
|
+
struct MyApp: App {
|
|
1045
|
+
init() {
|
|
1046
|
+
Amba.configure(apiKey: "YOUR_CLIENT_API_KEY")
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
var body: some Scene {
|
|
1050
|
+
WindowGroup {
|
|
1051
|
+
ContentView()
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Identify the user after login
|
|
1057
|
+
Amba.identify(userId: "user_123", properties: [
|
|
1058
|
+
"email": "user@example.com",
|
|
1059
|
+
"plan": "premium"
|
|
1060
|
+
])
|
|
1061
|
+
|
|
1062
|
+
// Track events
|
|
1063
|
+
Amba.track("workout_completed", properties: [
|
|
1064
|
+
"duration_minutes": 30,
|
|
1065
|
+
"type": "strength"
|
|
1066
|
+
])
|
|
1067
|
+
|
|
1068
|
+
// Register for push notifications
|
|
1069
|
+
Amba.Push.requestPermission()
|
|
1070
|
+
|
|
1071
|
+
// In AppDelegate:
|
|
1072
|
+
func application(_ application: UIApplication,
|
|
1073
|
+
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
1074
|
+
Amba.Push.registerToken(deviceToken)
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Get remote config
|
|
1078
|
+
let dailyLimit = Amba.Config.get("daily_limit", default: 10)
|
|
1079
|
+
|
|
1080
|
+
// Get current streak
|
|
1081
|
+
let streak = try await Amba.Streaks.get("daily_login")
|
|
1082
|
+
print("Current streak: \\(streak.currentCount)")`,
|
|
1083
|
+
android: `// Kotlin — Application class
|
|
1084
|
+
import dev.amba.sdk.Amba
|
|
1085
|
+
|
|
1086
|
+
class MyApp : Application() {
|
|
1087
|
+
override fun onCreate() {
|
|
1088
|
+
super.onCreate()
|
|
1089
|
+
Amba.configure(this, apiKey = "YOUR_CLIENT_API_KEY")
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Identify the user after login
|
|
1094
|
+
Amba.identify("user_123", mapOf(
|
|
1095
|
+
"email" to "user@example.com",
|
|
1096
|
+
"plan" to "premium"
|
|
1097
|
+
))
|
|
1098
|
+
|
|
1099
|
+
// Track events
|
|
1100
|
+
Amba.track("workout_completed", mapOf(
|
|
1101
|
+
"duration_minutes" to 30,
|
|
1102
|
+
"type" to "strength"
|
|
1103
|
+
))
|
|
1104
|
+
|
|
1105
|
+
// Register for push notifications
|
|
1106
|
+
Amba.Push.requestPermission(activity)
|
|
1107
|
+
|
|
1108
|
+
// Get remote config
|
|
1109
|
+
val dailyLimit = Amba.Config.get("daily_limit", default = 10)
|
|
1110
|
+
|
|
1111
|
+
// Get current streak
|
|
1112
|
+
val streak = Amba.Streaks.get("daily_login")
|
|
1113
|
+
println("Current streak: \${streak.currentCount}")`,
|
|
1114
|
+
"react-native": `// Bare React Native — App.tsx
|
|
1115
|
+
// For Expo apps, use @layers/amba-expo instead.
|
|
1116
|
+
import { Amba } from '@layers/amba-react-native';
|
|
1117
|
+
|
|
1118
|
+
// Configure once at app startup
|
|
1119
|
+
await Amba.configure({
|
|
1120
|
+
projectId: 'YOUR_PROJECT_ID',
|
|
1121
|
+
apiKey: 'YOUR_CLIENT_API_KEY',
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
// Track events
|
|
1125
|
+
await Amba.events.track('workout_completed', {
|
|
1126
|
+
duration_minutes: 30,
|
|
1127
|
+
type: 'strength',
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
// Register a push token (obtained from your native push library, e.g. @react-native-firebase/messaging)
|
|
1131
|
+
await Amba.push.register(deviceToken, 'ios');
|
|
1132
|
+
|
|
1133
|
+
// Read remote config
|
|
1134
|
+
const config = await Amba.config.fetch();
|
|
1135
|
+
|
|
1136
|
+
// Email sign-in
|
|
1137
|
+
await Amba.auth.signInWithEmail('user@example.com', 'hunter2');`,
|
|
1138
|
+
expo: `// Expo — app/_layout.tsx
|
|
1139
|
+
import { useEffect } from 'react';
|
|
1140
|
+
import { Slot } from 'expo-router';
|
|
1141
|
+
import { Amba } from '@layers/amba-expo';
|
|
1142
|
+
|
|
1143
|
+
export default function RootLayout() {
|
|
1144
|
+
useEffect(() => {
|
|
1145
|
+
Amba.configure({
|
|
1146
|
+
projectId: process.env.EXPO_PUBLIC_AMBA_PROJECT_ID!,
|
|
1147
|
+
apiKey: 'YOUR_CLIENT_API_KEY',
|
|
1148
|
+
});
|
|
1149
|
+
}, []);
|
|
1150
|
+
|
|
1151
|
+
return <Slot />;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// In any component — no hook required, Amba is a singleton
|
|
1155
|
+
import { Amba } from '@layers/amba-expo';
|
|
1156
|
+
|
|
1157
|
+
function HomeScreen() {
|
|
1158
|
+
const onComplete = async () => {
|
|
1159
|
+
await Amba.events.track('workout_completed', { type: 'strength' });
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
const onSignInApple = async () => {
|
|
1163
|
+
await Amba.signInWithApple();
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
return (
|
|
1167
|
+
<View>
|
|
1168
|
+
<Button title="Complete Workout" onPress={onComplete} />
|
|
1169
|
+
<Button title="Sign in with Apple" onPress={onSignInApple} />
|
|
1170
|
+
</View>
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Push + deep links — add to app.json:
|
|
1175
|
+
// {
|
|
1176
|
+
// "expo": {
|
|
1177
|
+
// "plugins": [
|
|
1178
|
+
// ["@layers/amba-expo", { "ios": { "pushNotifications": true } }]
|
|
1179
|
+
// ]
|
|
1180
|
+
// }
|
|
1181
|
+
// }`,
|
|
1182
|
+
flutter: `// Flutter — main.dart
|
|
1183
|
+
import 'package:amba_flutter/amba_flutter.dart';
|
|
1184
|
+
|
|
1185
|
+
void main() async {
|
|
1186
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
1187
|
+
await Amba.configure(apiKey: 'YOUR_CLIENT_API_KEY');
|
|
1188
|
+
runApp(const MyApp());
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Identify the user after login
|
|
1192
|
+
await Amba.identify('user_123', properties: {
|
|
1193
|
+
'email': 'user@example.com',
|
|
1194
|
+
'plan': 'premium',
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
// Track events
|
|
1198
|
+
await Amba.track('workout_completed', properties: {
|
|
1199
|
+
'duration_minutes': 30,
|
|
1200
|
+
'type': 'strength',
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
// Request push permission
|
|
1204
|
+
await Amba.push.requestPermission();
|
|
1205
|
+
|
|
1206
|
+
// Get remote config
|
|
1207
|
+
final dailyLimit = await Amba.config.get<int>('daily_limit', defaultValue: 10);
|
|
1208
|
+
|
|
1209
|
+
// Get current streak
|
|
1210
|
+
final streak = await Amba.streaks.get('daily_login');
|
|
1211
|
+
print('Current streak: \${streak.currentCount}');`
|
|
1212
|
+
};
|
|
1213
|
+
function registerTools$9(server, _apiClient) {
|
|
1214
|
+
server.tool("amba_get_sdk_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([
|
|
1215
|
+
"ios",
|
|
1216
|
+
"android",
|
|
1217
|
+
"react-native",
|
|
1218
|
+
"expo",
|
|
1219
|
+
"flutter"
|
|
1220
|
+
]).describe("The target platform for SDK integration instructions") }, async ({ platform }) => {
|
|
1221
|
+
const snippet = SDK_SNIPPETS[platform];
|
|
1222
|
+
if (!snippet) return { content: [{
|
|
1223
|
+
type: "text",
|
|
1224
|
+
text: `No setup instructions available for platform "${platform}". Supported platforms: ios, android, react-native, expo, flutter.`
|
|
1225
|
+
}] };
|
|
1226
|
+
return { content: [{
|
|
1227
|
+
type: "text",
|
|
1228
|
+
text: [
|
|
1229
|
+
`# Amba SDK Setup — ${platform}`,
|
|
1230
|
+
"",
|
|
1231
|
+
"## Installation",
|
|
1232
|
+
"",
|
|
1233
|
+
...platform === "ios" ? [
|
|
1234
|
+
"```",
|
|
1235
|
+
"// Swift Package Manager",
|
|
1236
|
+
"// Add: https://github.com/amba-dev/amba-ios.git",
|
|
1237
|
+
"```"
|
|
1238
|
+
] : platform === "android" ? [
|
|
1239
|
+
"```gradle",
|
|
1240
|
+
"implementation(\"dev.amba:amba-android:latest\")",
|
|
1241
|
+
"```"
|
|
1242
|
+
] : platform === "react-native" ? [
|
|
1243
|
+
"```bash",
|
|
1244
|
+
"npm install @layers/amba-react-native",
|
|
1245
|
+
"```"
|
|
1246
|
+
] : platform === "expo" ? [
|
|
1247
|
+
"```bash",
|
|
1248
|
+
"npx expo install @layers/amba-expo",
|
|
1249
|
+
"```"
|
|
1250
|
+
] : [
|
|
1251
|
+
"```yaml",
|
|
1252
|
+
"# pubspec.yaml",
|
|
1253
|
+
"dependencies:",
|
|
1254
|
+
" amba_flutter: ^latest",
|
|
1255
|
+
"```"
|
|
1256
|
+
],
|
|
1257
|
+
"",
|
|
1258
|
+
"## Integration Code",
|
|
1259
|
+
"",
|
|
1260
|
+
"```" + (platform === "ios" ? "swift" : platform === "android" ? "kotlin" : platform === "flutter" ? "dart" : "typescript"),
|
|
1261
|
+
snippet,
|
|
1262
|
+
"```",
|
|
1263
|
+
"",
|
|
1264
|
+
"## Next Steps",
|
|
1265
|
+
"",
|
|
1266
|
+
"1. Replace YOUR_CLIENT_API_KEY with your project's client API key (find it with amba_get_project)",
|
|
1267
|
+
"2. Configure push notification integration (APNs for iOS, FCM for Android) using amba_configure_integration",
|
|
1268
|
+
"3. Create segments to target user groups with amba_create_segment",
|
|
1269
|
+
"4. Set up remote config values with amba_set_config",
|
|
1270
|
+
"5. Create streaks to track user engagement with amba_create_streak"
|
|
1271
|
+
].join("\n")
|
|
1272
|
+
}] };
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
//#endregion
|
|
1276
|
+
//#region src/tools/achievements.ts
|
|
1277
|
+
const criteriaSchema = z.object({
|
|
1278
|
+
type: z.enum([
|
|
1279
|
+
"event_count",
|
|
1280
|
+
"streak_length",
|
|
1281
|
+
"xp_threshold",
|
|
1282
|
+
"property_value"
|
|
1283
|
+
]).describe("Type of criteria: event_count (track X N times), streak_length (N-day streak), xp_threshold (reach N XP), property_value (user property equals N)"),
|
|
1284
|
+
event_name: z.string().optional().describe("Event name (required for event_count type)"),
|
|
1285
|
+
streak_definition_id: z.string().optional().describe("Streak definition ID (required for streak_length type)"),
|
|
1286
|
+
property_key: z.string().optional().describe("User property key (required for property_value type)"),
|
|
1287
|
+
target_value: z.number().describe("Target value to unlock the achievement")
|
|
1288
|
+
});
|
|
1289
|
+
function registerTools$8(server, apiClient) {
|
|
1290
|
+
server.tool("amba_create_achievement", "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.", {
|
|
1291
|
+
project_id: z.string().describe("The project ID"),
|
|
1292
|
+
key: z.string().describe("Unique key for this achievement (e.g. \"first_workout\", \"streak_master_7\")"),
|
|
1293
|
+
name: z.string().describe("Display name (e.g. \"First Workout\", \"Streak Master\")"),
|
|
1294
|
+
description: z.string().optional().describe("Description shown to users"),
|
|
1295
|
+
icon_url: z.string().optional().describe("URL to the achievement badge/icon image"),
|
|
1296
|
+
xp_reward: z.number().optional().describe("Bonus XP to award when the achievement is unlocked (default 0)"),
|
|
1297
|
+
criteria: criteriaSchema.describe("Unlock criteria for the achievement"),
|
|
1298
|
+
is_hidden: z.boolean().optional().describe("Whether this achievement is hidden until unlocked (default false)"),
|
|
1299
|
+
sort_order: z.number().optional().describe("Display order (lower numbers appear first)")
|
|
1300
|
+
}, async ({ project_id, key, name, description, icon_url, xp_reward, criteria, is_hidden, sort_order }) => {
|
|
1301
|
+
const payload = {
|
|
1302
|
+
key,
|
|
1303
|
+
name,
|
|
1304
|
+
criteria
|
|
1305
|
+
};
|
|
1306
|
+
if (description !== void 0) payload.description = description;
|
|
1307
|
+
if (icon_url !== void 0) payload.icon_url = icon_url;
|
|
1308
|
+
if (xp_reward !== void 0) payload.xp_reward = xp_reward;
|
|
1309
|
+
if (is_hidden !== void 0) payload.is_hidden = is_hidden;
|
|
1310
|
+
if (sort_order !== void 0) payload.sort_order = sort_order;
|
|
1311
|
+
const result = await apiClient.post(`/projects/${project_id}/achievements`, payload);
|
|
1312
|
+
return { content: [{
|
|
1313
|
+
type: "text",
|
|
1314
|
+
text: JSON.stringify(result, null, 2)
|
|
1315
|
+
}] };
|
|
1316
|
+
});
|
|
1317
|
+
server.tool("amba_list_achievements", "List all achievement definitions for a project, including criteria and XP rewards.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
1318
|
+
const result = await apiClient.get(`/projects/${project_id}/achievements`);
|
|
1319
|
+
return { content: [{
|
|
1320
|
+
type: "text",
|
|
1321
|
+
text: JSON.stringify(result, null, 2)
|
|
1322
|
+
}] };
|
|
1323
|
+
});
|
|
1324
|
+
server.tool("amba_get_achievement", "Fetch a single achievement definition by ID, including criteria and XP reward.", {
|
|
1325
|
+
project_id: z.string().describe("The project ID"),
|
|
1326
|
+
achievement_id: z.string().describe("The achievement ID")
|
|
1327
|
+
}, async ({ project_id, achievement_id }) => {
|
|
1328
|
+
const result = await apiClient.get(`/projects/${project_id}/achievements/${achievement_id}`);
|
|
1329
|
+
return { content: [{
|
|
1330
|
+
type: "text",
|
|
1331
|
+
text: JSON.stringify(result, null, 2)
|
|
1332
|
+
}] };
|
|
1333
|
+
});
|
|
1334
|
+
server.tool("amba_update_achievement", "Partially update an achievement definition. Only supply the fields you want to change.", {
|
|
1335
|
+
project_id: z.string().describe("The project ID"),
|
|
1336
|
+
achievement_id: z.string().describe("The achievement ID"),
|
|
1337
|
+
name: z.string().optional().describe("Display name"),
|
|
1338
|
+
description: z.string().optional().describe("Description shown to users"),
|
|
1339
|
+
icon_url: z.string().optional().describe("URL to the achievement badge/icon image"),
|
|
1340
|
+
xp_reward: z.number().optional().describe("Bonus XP awarded on unlock"),
|
|
1341
|
+
criteria: criteriaSchema.optional().describe("New unlock criteria"),
|
|
1342
|
+
is_hidden: z.boolean().optional().describe("Whether the achievement is hidden until unlocked"),
|
|
1343
|
+
sort_order: z.number().optional().describe("Display order")
|
|
1344
|
+
}, async ({ project_id, achievement_id, name, description, icon_url, xp_reward, criteria, is_hidden, sort_order }) => {
|
|
1345
|
+
const payload = {};
|
|
1346
|
+
if (name !== void 0) payload.name = name;
|
|
1347
|
+
if (description !== void 0) payload.description = description;
|
|
1348
|
+
if (icon_url !== void 0) payload.icon_url = icon_url;
|
|
1349
|
+
if (xp_reward !== void 0) payload.xp_reward = xp_reward;
|
|
1350
|
+
if (criteria !== void 0) payload.criteria = criteria;
|
|
1351
|
+
if (is_hidden !== void 0) payload.is_hidden = is_hidden;
|
|
1352
|
+
if (sort_order !== void 0) payload.sort_order = sort_order;
|
|
1353
|
+
const result = await apiClient.patch(`/projects/${project_id}/achievements/${achievement_id}`, payload);
|
|
1354
|
+
return { content: [{
|
|
1355
|
+
type: "text",
|
|
1356
|
+
text: JSON.stringify(result, null, 2)
|
|
1357
|
+
}] };
|
|
1358
|
+
});
|
|
1359
|
+
server.tool("amba_delete_achievement", "Delete an achievement definition. Existing user unlocks are not removed; the row is hard-deleted from `achievements`.", {
|
|
1360
|
+
project_id: z.string().describe("The project ID"),
|
|
1361
|
+
achievement_id: z.string().describe("The achievement ID")
|
|
1362
|
+
}, async ({ project_id, achievement_id }) => {
|
|
1363
|
+
const result = await apiClient.delete(`/projects/${project_id}/achievements/${achievement_id}`);
|
|
1364
|
+
return { content: [{
|
|
1365
|
+
type: "text",
|
|
1366
|
+
text: JSON.stringify(result, null, 2)
|
|
1367
|
+
}] };
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
//#endregion
|
|
1371
|
+
//#region src/tools/challenges.ts
|
|
1372
|
+
function registerTools$7(server, apiClient) {
|
|
1373
|
+
server.tool("amba_create_challenge", "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.", {
|
|
1374
|
+
project_id: z.string().describe("The project ID"),
|
|
1375
|
+
name: z.string().describe("Challenge name (e.g. \"7-Day Fitness Sprint\", \"XP Weekend Blitz\")"),
|
|
1376
|
+
description: z.string().optional().describe("Description shown to users"),
|
|
1377
|
+
start_at: z.string().describe("ISO 8601 timestamp when the challenge starts"),
|
|
1378
|
+
end_at: z.string().describe("ISO 8601 timestamp when the challenge ends"),
|
|
1379
|
+
goal_type: z.enum([
|
|
1380
|
+
"event_count",
|
|
1381
|
+
"xp_earned",
|
|
1382
|
+
"streak_maintained"
|
|
1383
|
+
]).describe("Type of goal: event_count (track N events), xp_earned (earn N XP), streak_maintained (keep streak for N days)"),
|
|
1384
|
+
goal_value: z.number().describe("Target value for the goal"),
|
|
1385
|
+
reward_xp: z.number().optional().describe("XP to award on challenge completion (default 0)"),
|
|
1386
|
+
reward_achievement_id: z.string().optional().describe("Achievement to unlock on challenge completion")
|
|
1387
|
+
}, async ({ project_id, name, description, start_at, end_at, goal_type, goal_value, reward_xp, reward_achievement_id }) => {
|
|
1388
|
+
const payload = {
|
|
1389
|
+
name,
|
|
1390
|
+
start_at,
|
|
1391
|
+
end_at,
|
|
1392
|
+
goal_type,
|
|
1393
|
+
goal_value
|
|
1394
|
+
};
|
|
1395
|
+
if (description !== void 0) payload.description = description;
|
|
1396
|
+
if (reward_xp !== void 0) payload.reward_xp = reward_xp;
|
|
1397
|
+
if (reward_achievement_id !== void 0) payload.reward_achievement_id = reward_achievement_id;
|
|
1398
|
+
const result = await apiClient.post(`/projects/${project_id}/challenges`, payload);
|
|
1399
|
+
return { content: [{
|
|
1400
|
+
type: "text",
|
|
1401
|
+
text: JSON.stringify(result, null, 2)
|
|
1402
|
+
}] };
|
|
1403
|
+
});
|
|
1404
|
+
server.tool("amba_list_challenges", "List challenge definitions for a project. Optionally filter by status (active, upcoming, ended).", {
|
|
1405
|
+
project_id: z.string().describe("The project ID"),
|
|
1406
|
+
status: z.enum([
|
|
1407
|
+
"active",
|
|
1408
|
+
"upcoming",
|
|
1409
|
+
"ended"
|
|
1410
|
+
]).optional().describe("Filter by challenge status")
|
|
1411
|
+
}, async ({ project_id, status }) => {
|
|
1412
|
+
const query = {};
|
|
1413
|
+
if (status !== void 0) query.status = status;
|
|
1414
|
+
const result = await apiClient.get(`/projects/${project_id}/challenges`, query);
|
|
1415
|
+
return { content: [{
|
|
1416
|
+
type: "text",
|
|
1417
|
+
text: JSON.stringify(result, null, 2)
|
|
1418
|
+
}] };
|
|
1419
|
+
});
|
|
1420
|
+
server.tool("amba_get_challenge", "Fetch a single challenge by ID, including timing, goal, and reward configuration.", {
|
|
1421
|
+
project_id: z.string().describe("The project ID"),
|
|
1422
|
+
challenge_id: z.string().describe("The challenge ID")
|
|
1423
|
+
}, async ({ project_id, challenge_id }) => {
|
|
1424
|
+
const result = await apiClient.get(`/projects/${project_id}/challenges/${challenge_id}`);
|
|
1425
|
+
return { content: [{
|
|
1426
|
+
type: "text",
|
|
1427
|
+
text: JSON.stringify(result, null, 2)
|
|
1428
|
+
}] };
|
|
1429
|
+
});
|
|
1430
|
+
server.tool("amba_update_challenge", "Partially update a challenge definition. Only the supplied fields are touched.", {
|
|
1431
|
+
project_id: z.string().describe("The project ID"),
|
|
1432
|
+
challenge_id: z.string().describe("The challenge ID"),
|
|
1433
|
+
name: z.string().optional().describe("Challenge name"),
|
|
1434
|
+
description: z.string().optional().describe("Description shown to users"),
|
|
1435
|
+
start_at: z.string().optional().describe("ISO 8601 start timestamp"),
|
|
1436
|
+
end_at: z.string().optional().describe("ISO 8601 end timestamp"),
|
|
1437
|
+
goal_value: z.number().optional().describe("Target value for the goal"),
|
|
1438
|
+
reward_xp: z.number().optional().describe("XP awarded on completion"),
|
|
1439
|
+
reward_achievement_id: z.string().optional().describe("Achievement to unlock on completion")
|
|
1440
|
+
}, async ({ project_id, challenge_id, name, description, start_at, end_at, goal_value, reward_xp, reward_achievement_id }) => {
|
|
1441
|
+
const payload = {};
|
|
1442
|
+
if (name !== void 0) payload.name = name;
|
|
1443
|
+
if (description !== void 0) payload.description = description;
|
|
1444
|
+
if (start_at !== void 0) payload.start_at = start_at;
|
|
1445
|
+
if (end_at !== void 0) payload.end_at = end_at;
|
|
1446
|
+
if (goal_value !== void 0) payload.goal_value = goal_value;
|
|
1447
|
+
if (reward_xp !== void 0) payload.reward_xp = reward_xp;
|
|
1448
|
+
if (reward_achievement_id !== void 0) payload.reward_achievement_id = reward_achievement_id;
|
|
1449
|
+
const result = await apiClient.patch(`/projects/${project_id}/challenges/${challenge_id}`, payload);
|
|
1450
|
+
return { content: [{
|
|
1451
|
+
type: "text",
|
|
1452
|
+
text: JSON.stringify(result, null, 2)
|
|
1453
|
+
}] };
|
|
1454
|
+
});
|
|
1455
|
+
server.tool("amba_delete_challenge", "Delete a challenge definition. Hard delete — the row is removed; existing participant rows are cleaned up.", {
|
|
1456
|
+
project_id: z.string().describe("The project ID"),
|
|
1457
|
+
challenge_id: z.string().describe("The challenge ID")
|
|
1458
|
+
}, async ({ project_id, challenge_id }) => {
|
|
1459
|
+
const result = await apiClient.delete(`/projects/${project_id}/challenges/${challenge_id}`);
|
|
1460
|
+
return { content: [{
|
|
1461
|
+
type: "text",
|
|
1462
|
+
text: JSON.stringify(result, null, 2)
|
|
1463
|
+
}] };
|
|
1464
|
+
});
|
|
1465
|
+
server.tool("amba_list_challenge_participants", "List participants of a challenge with their progress toward the goal. Useful for inspecting who is participating and how close they are.", {
|
|
1466
|
+
project_id: z.string().describe("The project ID"),
|
|
1467
|
+
challenge_id: z.string().describe("The challenge ID"),
|
|
1468
|
+
limit: z.number().optional().describe("Max number of participants to return."),
|
|
1469
|
+
offset: z.number().optional().describe("Offset for pagination.")
|
|
1470
|
+
}, async ({ project_id, challenge_id, limit, offset }) => {
|
|
1471
|
+
const query = {};
|
|
1472
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
1473
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
1474
|
+
const result = await apiClient.get(`/projects/${project_id}/challenges/${challenge_id}/participants`, query);
|
|
1475
|
+
return { content: [{
|
|
1476
|
+
type: "text",
|
|
1477
|
+
text: JSON.stringify(result, null, 2)
|
|
1478
|
+
}] };
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
//#endregion
|
|
1482
|
+
//#region src/tools/economy.ts
|
|
1483
|
+
function registerTools$6(server, apiClient) {
|
|
1484
|
+
server.tool("amba_create_currency", "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.", {
|
|
1485
|
+
project_id: z.string().describe("The project ID"),
|
|
1486
|
+
code: z.string().describe("Unique currency code (e.g. \"gold\", \"gems\", \"hearts\")"),
|
|
1487
|
+
name: z.string().describe("Display name (e.g. \"Gold Coins\", \"Gems\", \"Hearts\")"),
|
|
1488
|
+
description: z.string().optional().describe("Description of the currency"),
|
|
1489
|
+
is_premium: z.boolean().optional().describe("Whether this is a premium/hard currency (purchased with real money). Defaults to false."),
|
|
1490
|
+
initial_balance: z.number().optional().describe("Starting balance for new users. Defaults to 0."),
|
|
1491
|
+
max_balance: z.number().optional().describe("Maximum balance cap (null for unlimited)"),
|
|
1492
|
+
auto_recharge_amount: z.number().optional().describe("Amount to auto-recharge (e.g. 1 heart every N hours)"),
|
|
1493
|
+
auto_recharge_interval_hours: z.number().optional().describe("Hours between auto-recharges")
|
|
1494
|
+
}, async ({ project_id, code, name, description, is_premium, initial_balance, max_balance, auto_recharge_amount, auto_recharge_interval_hours }) => {
|
|
1495
|
+
const payload = {
|
|
1496
|
+
code,
|
|
1497
|
+
name
|
|
1498
|
+
};
|
|
1499
|
+
if (description !== void 0) payload.description = description;
|
|
1500
|
+
if (is_premium !== void 0) payload.is_premium = is_premium;
|
|
1501
|
+
if (initial_balance !== void 0) payload.initial_balance = initial_balance;
|
|
1502
|
+
if (max_balance !== void 0) payload.max_balance = max_balance;
|
|
1503
|
+
if (auto_recharge_amount !== void 0) payload.auto_recharge_amount = auto_recharge_amount;
|
|
1504
|
+
if (auto_recharge_interval_hours !== void 0) payload.auto_recharge_interval_hours = auto_recharge_interval_hours;
|
|
1505
|
+
const result = await apiClient.post(`/projects/${project_id}/currencies`, payload);
|
|
1506
|
+
return { content: [{
|
|
1507
|
+
type: "text",
|
|
1508
|
+
text: JSON.stringify(result, null, 2)
|
|
1509
|
+
}] };
|
|
1510
|
+
});
|
|
1511
|
+
server.tool("amba_list_currencies", "List all virtual currency definitions for a project.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
1512
|
+
const result = await apiClient.get(`/projects/${project_id}/currencies`);
|
|
1513
|
+
return { content: [{
|
|
1514
|
+
type: "text",
|
|
1515
|
+
text: JSON.stringify(result, null, 2)
|
|
1516
|
+
}] };
|
|
1517
|
+
});
|
|
1518
|
+
server.tool("amba_grant_currency", "Grant virtual currency to a specific user. Useful for rewards, promotions, or admin adjustments.", {
|
|
1519
|
+
project_id: z.string().describe("The project ID"),
|
|
1520
|
+
app_user_id: z.string().describe("The user to grant currency to"),
|
|
1521
|
+
currency_code: z.string().describe("Currency code (e.g. \"gold\")"),
|
|
1522
|
+
amount: z.number().describe("Amount to grant (positive integer)"),
|
|
1523
|
+
reason: z.string().optional().describe("Reason for the grant (for audit trail)")
|
|
1524
|
+
}, async ({ project_id, app_user_id, currency_code, amount, reason }) => {
|
|
1525
|
+
const payload = {
|
|
1526
|
+
app_user_id,
|
|
1527
|
+
currency_code,
|
|
1528
|
+
amount
|
|
1529
|
+
};
|
|
1530
|
+
if (reason !== void 0) payload.reason = reason;
|
|
1531
|
+
const result = await apiClient.post(`/projects/${project_id}/currencies/grant`, payload);
|
|
1532
|
+
return { content: [{
|
|
1533
|
+
type: "text",
|
|
1534
|
+
text: JSON.stringify(result, null, 2)
|
|
1535
|
+
}] };
|
|
1536
|
+
});
|
|
1537
|
+
server.tool("amba_create_catalog_item", "Create a catalog item for a project. Items can be durable (owned forever), consumable (used up), or bundles (contain other items).", {
|
|
1538
|
+
project_id: z.string().describe("The project ID"),
|
|
1539
|
+
key: z.string().describe("Unique item key (e.g. \"premium_theme\", \"extra_life\", \"starter_pack\")"),
|
|
1540
|
+
name: z.string().describe("Display name"),
|
|
1541
|
+
description: z.string().optional().describe("Item description"),
|
|
1542
|
+
icon_url: z.string().optional().describe("URL to the item icon"),
|
|
1543
|
+
category: z.string().optional().describe("Category for filtering (e.g. \"themes\", \"powerups\", \"packs\")"),
|
|
1544
|
+
tags: z.array(z.string()).optional().describe("Tags for filtering"),
|
|
1545
|
+
item_type: z.enum([
|
|
1546
|
+
"durable",
|
|
1547
|
+
"consumable",
|
|
1548
|
+
"bundle"
|
|
1549
|
+
]).optional().describe("Item type. Defaults to \"durable\"."),
|
|
1550
|
+
metadata: z.record(z.unknown()).optional().describe("Additional metadata (JSONB)")
|
|
1551
|
+
}, async ({ project_id, key, name, description, icon_url, category, tags, item_type, metadata }) => {
|
|
1552
|
+
const payload = {
|
|
1553
|
+
key,
|
|
1554
|
+
name
|
|
1555
|
+
};
|
|
1556
|
+
if (description !== void 0) payload.description = description;
|
|
1557
|
+
if (icon_url !== void 0) payload.icon_url = icon_url;
|
|
1558
|
+
if (category !== void 0) payload.category = category;
|
|
1559
|
+
if (tags !== void 0) payload.tags = tags;
|
|
1560
|
+
if (item_type !== void 0) payload.item_type = item_type;
|
|
1561
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
1562
|
+
const result = await apiClient.post(`/projects/${project_id}/catalog`, payload);
|
|
1563
|
+
return { content: [{
|
|
1564
|
+
type: "text",
|
|
1565
|
+
text: JSON.stringify(result, null, 2)
|
|
1566
|
+
}] };
|
|
1567
|
+
});
|
|
1568
|
+
server.tool("amba_list_catalog", "List all catalog items for a project, optionally filtered by category.", {
|
|
1569
|
+
project_id: z.string().describe("The project ID"),
|
|
1570
|
+
category: z.string().optional().describe("Filter by category")
|
|
1571
|
+
}, async ({ project_id, category }) => {
|
|
1572
|
+
const query = {};
|
|
1573
|
+
if (category) query.category = category;
|
|
1574
|
+
const result = await apiClient.get(`/projects/${project_id}/catalog`, query);
|
|
1575
|
+
return { content: [{
|
|
1576
|
+
type: "text",
|
|
1577
|
+
text: JSON.stringify(result, null, 2)
|
|
1578
|
+
}] };
|
|
1579
|
+
});
|
|
1580
|
+
server.tool("amba_set_item_price", "Set the price of a catalog item in a specific virtual currency.", {
|
|
1581
|
+
project_id: z.string().describe("The project ID"),
|
|
1582
|
+
catalog_item_id: z.string().describe("The catalog item ID"),
|
|
1583
|
+
currency_code: z.string().describe("Currency code (e.g. \"gold\", \"gems\")"),
|
|
1584
|
+
price: z.number().describe("Price in the specified currency")
|
|
1585
|
+
}, async ({ project_id, catalog_item_id, currency_code, price }) => {
|
|
1586
|
+
const result = await apiClient.post(`/projects/${project_id}/catalog/${catalog_item_id}/prices`, {
|
|
1587
|
+
currency_code,
|
|
1588
|
+
price
|
|
1589
|
+
});
|
|
1590
|
+
return { content: [{
|
|
1591
|
+
type: "text",
|
|
1592
|
+
text: JSON.stringify(result, null, 2)
|
|
1593
|
+
}] };
|
|
1594
|
+
});
|
|
1595
|
+
server.tool("amba_create_store", "Create a store for a project. Stores are curated subsets of the catalog, optionally targeted at specific user segments.", {
|
|
1596
|
+
project_id: z.string().describe("The project ID"),
|
|
1597
|
+
name: z.string().describe("Store name (e.g. \"Main Shop\", \"Premium Store\", \"Daily Deals\")"),
|
|
1598
|
+
description: z.string().optional().describe("Store description"),
|
|
1599
|
+
segment_id: z.string().optional().describe("Target segment ID (only users in this segment see the store)")
|
|
1600
|
+
}, async ({ project_id, name, description, segment_id }) => {
|
|
1601
|
+
const payload = { name };
|
|
1602
|
+
if (description !== void 0) payload.description = description;
|
|
1603
|
+
if (segment_id !== void 0) payload.segment_id = segment_id;
|
|
1604
|
+
const result = await apiClient.post(`/projects/${project_id}/stores`, payload);
|
|
1605
|
+
return { content: [{
|
|
1606
|
+
type: "text",
|
|
1607
|
+
text: JSON.stringify(result, null, 2)
|
|
1608
|
+
}] };
|
|
1609
|
+
});
|
|
1610
|
+
server.tool("amba_add_store_listing", "Add a catalog item to a store with optional price overrides, scheduling, and purchase limits.", {
|
|
1611
|
+
project_id: z.string().describe("The project ID"),
|
|
1612
|
+
store_id: z.string().describe("The store ID"),
|
|
1613
|
+
catalog_item_id: z.string().describe("The catalog item ID to list"),
|
|
1614
|
+
override_prices: z.record(z.number()).optional().describe("Price overrides by currency code (e.g. {\"gold\": 50, \"gems\": 5})"),
|
|
1615
|
+
sort_order: z.number().optional().describe("Sort order in the store (lower = first)"),
|
|
1616
|
+
available_from: z.string().optional().describe("ISO 8601 timestamp when the listing becomes available"),
|
|
1617
|
+
available_until: z.string().optional().describe("ISO 8601 timestamp when the listing expires"),
|
|
1618
|
+
max_purchases_per_user: z.number().optional().describe("Maximum number of times each user can purchase this listing")
|
|
1619
|
+
}, async ({ project_id, store_id, catalog_item_id, override_prices, sort_order, available_from, available_until, max_purchases_per_user }) => {
|
|
1620
|
+
const payload = { catalog_item_id };
|
|
1621
|
+
if (override_prices !== void 0) payload.override_prices = override_prices;
|
|
1622
|
+
if (sort_order !== void 0) payload.sort_order = sort_order;
|
|
1623
|
+
if (available_from !== void 0) payload.available_from = available_from;
|
|
1624
|
+
if (available_until !== void 0) payload.available_until = available_until;
|
|
1625
|
+
if (max_purchases_per_user !== void 0) payload.max_purchases_per_user = max_purchases_per_user;
|
|
1626
|
+
const result = await apiClient.post(`/projects/${project_id}/stores/${store_id}/listings`, payload);
|
|
1627
|
+
return { content: [{
|
|
1628
|
+
type: "text",
|
|
1629
|
+
text: JSON.stringify(result, null, 2)
|
|
1630
|
+
}] };
|
|
1631
|
+
});
|
|
1632
|
+
server.tool("amba_grant_item", "Grant a catalog item directly to a user's inventory. Bypasses the purchase flow.", {
|
|
1633
|
+
project_id: z.string().describe("The project ID"),
|
|
1634
|
+
app_user_id: z.string().describe("The user to grant the item to"),
|
|
1635
|
+
catalog_item_id: z.string().describe("The catalog item ID to grant"),
|
|
1636
|
+
quantity: z.number().optional().describe("Quantity to grant (default 1)")
|
|
1637
|
+
}, async ({ project_id, app_user_id, catalog_item_id, quantity }) => {
|
|
1638
|
+
const payload = {
|
|
1639
|
+
app_user_id,
|
|
1640
|
+
catalog_item_id
|
|
1641
|
+
};
|
|
1642
|
+
if (quantity !== void 0) payload.quantity = quantity;
|
|
1643
|
+
const result = await apiClient.post(`/projects/${project_id}/inventory/grant`, payload);
|
|
1644
|
+
return { content: [{
|
|
1645
|
+
type: "text",
|
|
1646
|
+
text: JSON.stringify(result, null, 2)
|
|
1647
|
+
}] };
|
|
1648
|
+
});
|
|
1649
|
+
server.tool("amba_get_user_inventory", "View a user's inventory including all owned items and quantities.", {
|
|
1650
|
+
project_id: z.string().describe("The project ID"),
|
|
1651
|
+
app_user_id: z.string().describe("The user ID to look up")
|
|
1652
|
+
}, async ({ project_id, app_user_id }) => {
|
|
1653
|
+
const result = await apiClient.get(`/projects/${project_id}/inventory/${app_user_id}`);
|
|
1654
|
+
return { content: [{
|
|
1655
|
+
type: "text",
|
|
1656
|
+
text: JSON.stringify(result, null, 2)
|
|
1657
|
+
}] };
|
|
1658
|
+
});
|
|
1659
|
+
server.tool("amba_create_referral_program", "Create a referral/promo code with configurable rewards for both referrer and referee. Rewards are granted as virtual currency.", {
|
|
1660
|
+
project_id: z.string().describe("The project ID"),
|
|
1661
|
+
code: z.string().optional().describe("Custom code (auto-generated if not provided)"),
|
|
1662
|
+
reward_referrer: z.record(z.unknown()).optional().describe("Rewards for the referrer (e.g. {\"gold\": 100, \"gems\": 5})"),
|
|
1663
|
+
reward_referee: z.record(z.unknown()).optional().describe("Rewards for the referee (e.g. {\"gold\": 50})"),
|
|
1664
|
+
max_uses: z.number().optional().describe("Maximum number of times this code can be used")
|
|
1665
|
+
}, async ({ project_id, code, reward_referrer, reward_referee, max_uses }) => {
|
|
1666
|
+
const payload = {};
|
|
1667
|
+
if (code !== void 0) payload.code = code;
|
|
1668
|
+
if (reward_referrer !== void 0) payload.reward_referrer = reward_referrer;
|
|
1669
|
+
if (reward_referee !== void 0) payload.reward_referee = reward_referee;
|
|
1670
|
+
if (max_uses !== void 0) payload.max_uses = max_uses;
|
|
1671
|
+
const result = await apiClient.post(`/projects/${project_id}/referrals`, payload);
|
|
1672
|
+
return { content: [{
|
|
1673
|
+
type: "text",
|
|
1674
|
+
text: JSON.stringify(result, null, 2)
|
|
1675
|
+
}] };
|
|
1676
|
+
});
|
|
1677
|
+
server.tool("amba_get_referral_stats", "Get stats for a specific referral code including total claims and claim history.", {
|
|
1678
|
+
project_id: z.string().describe("The project ID"),
|
|
1679
|
+
referral_code_id: z.string().describe("The referral code ID")
|
|
1680
|
+
}, async ({ project_id, referral_code_id }) => {
|
|
1681
|
+
const result = await apiClient.get(`/projects/${project_id}/referrals/${referral_code_id}/stats`);
|
|
1682
|
+
return { content: [{
|
|
1683
|
+
type: "text",
|
|
1684
|
+
text: JSON.stringify(result, null, 2)
|
|
1685
|
+
}] };
|
|
1686
|
+
});
|
|
1687
|
+
server.tool("amba_referrals_list", "List all referral/promo codes for a project, newest first.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
1688
|
+
const result = await apiClient.get(`/projects/${project_id}/referrals`);
|
|
1689
|
+
return { content: [{
|
|
1690
|
+
type: "text",
|
|
1691
|
+
text: JSON.stringify(result, null, 2)
|
|
1692
|
+
}] };
|
|
1693
|
+
});
|
|
1694
|
+
server.tool("amba_referrals_patch", "Update a referral code. Only the supplied fields are changed; omitted fields are left untouched.", {
|
|
1695
|
+
project_id: z.string().describe("The project ID"),
|
|
1696
|
+
referral_code_id: z.string().describe("The referral code ID"),
|
|
1697
|
+
reward_referrer: z.record(z.unknown()).optional().describe("New rewards for the referrer"),
|
|
1698
|
+
reward_referee: z.record(z.unknown()).optional().describe("New rewards for the referee"),
|
|
1699
|
+
max_uses: z.number().optional().describe("New max usage cap"),
|
|
1700
|
+
is_active: z.boolean().optional().describe("Toggle whether the code is active")
|
|
1701
|
+
}, async ({ project_id, referral_code_id, reward_referrer, reward_referee, max_uses, is_active }) => {
|
|
1702
|
+
const payload = {};
|
|
1703
|
+
if (reward_referrer !== void 0) payload.reward_referrer = reward_referrer;
|
|
1704
|
+
if (reward_referee !== void 0) payload.reward_referee = reward_referee;
|
|
1705
|
+
if (max_uses !== void 0) payload.max_uses = max_uses;
|
|
1706
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
1707
|
+
const result = await apiClient.patch(`/projects/${project_id}/referrals/${referral_code_id}`, payload);
|
|
1708
|
+
return { content: [{
|
|
1709
|
+
type: "text",
|
|
1710
|
+
text: JSON.stringify(result, null, 2)
|
|
1711
|
+
}] };
|
|
1712
|
+
});
|
|
1713
|
+
server.tool("amba_referrals_delete", "Delete a referral code. Existing claim rows are preserved; the code can no longer be redeemed.", {
|
|
1714
|
+
project_id: z.string().describe("The project ID"),
|
|
1715
|
+
referral_code_id: z.string().describe("The referral code ID")
|
|
1716
|
+
}, async ({ project_id, referral_code_id }) => {
|
|
1717
|
+
const result = await apiClient.delete(`/projects/${project_id}/referrals/${referral_code_id}`);
|
|
1718
|
+
return { content: [{
|
|
1719
|
+
type: "text",
|
|
1720
|
+
text: JSON.stringify(result, null, 2)
|
|
1721
|
+
}] };
|
|
1722
|
+
});
|
|
1723
|
+
server.tool("amba_stores_list", "List all stores for a project, ordered by name.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
1724
|
+
const result = await apiClient.get(`/projects/${project_id}/stores`);
|
|
1725
|
+
return { content: [{
|
|
1726
|
+
type: "text",
|
|
1727
|
+
text: JSON.stringify(result, null, 2)
|
|
1728
|
+
}] };
|
|
1729
|
+
});
|
|
1730
|
+
server.tool("amba_stores_patch", "Update a store. Only the supplied fields are changed.", {
|
|
1731
|
+
project_id: z.string().describe("The project ID"),
|
|
1732
|
+
store_id: z.string().describe("The store ID"),
|
|
1733
|
+
name: z.string().optional().describe("New store name"),
|
|
1734
|
+
description: z.string().optional().describe("New store description"),
|
|
1735
|
+
is_active: z.boolean().optional().describe("Toggle store visibility"),
|
|
1736
|
+
segment_id: z.string().optional().describe("Target segment ID (only users in this segment see the store)")
|
|
1737
|
+
}, async ({ project_id, store_id, name, description, is_active, segment_id }) => {
|
|
1738
|
+
const payload = {};
|
|
1739
|
+
if (name !== void 0) payload.name = name;
|
|
1740
|
+
if (description !== void 0) payload.description = description;
|
|
1741
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
1742
|
+
if (segment_id !== void 0) payload.segment_id = segment_id;
|
|
1743
|
+
const result = await apiClient.patch(`/projects/${project_id}/stores/${store_id}`, payload);
|
|
1744
|
+
return { content: [{
|
|
1745
|
+
type: "text",
|
|
1746
|
+
text: JSON.stringify(result, null, 2)
|
|
1747
|
+
}] };
|
|
1748
|
+
});
|
|
1749
|
+
server.tool("amba_stores_delete", "Delete a store. Its listings are deleted as well; historical purchases are preserved.", {
|
|
1750
|
+
project_id: z.string().describe("The project ID"),
|
|
1751
|
+
store_id: z.string().describe("The store ID")
|
|
1752
|
+
}, async ({ project_id, store_id }) => {
|
|
1753
|
+
const result = await apiClient.delete(`/projects/${project_id}/stores/${store_id}`);
|
|
1754
|
+
return { content: [{
|
|
1755
|
+
type: "text",
|
|
1756
|
+
text: JSON.stringify(result, null, 2)
|
|
1757
|
+
}] };
|
|
1758
|
+
});
|
|
1759
|
+
server.tool("amba_stores_list_listings", "List all listings in a store, including the underlying catalog item details, ordered by sort_order.", {
|
|
1760
|
+
project_id: z.string().describe("The project ID"),
|
|
1761
|
+
store_id: z.string().describe("The store ID")
|
|
1762
|
+
}, async ({ project_id, store_id }) => {
|
|
1763
|
+
const result = await apiClient.get(`/projects/${project_id}/stores/${store_id}/listings`);
|
|
1764
|
+
return { content: [{
|
|
1765
|
+
type: "text",
|
|
1766
|
+
text: JSON.stringify(result, null, 2)
|
|
1767
|
+
}] };
|
|
1768
|
+
});
|
|
1769
|
+
server.tool("amba_stores_patch_listing", "Update a store listing (price overrides, sort order, availability window, purchase caps).", {
|
|
1770
|
+
project_id: z.string().describe("The project ID"),
|
|
1771
|
+
store_id: z.string().describe("The store ID"),
|
|
1772
|
+
listing_id: z.string().describe("The store listing ID"),
|
|
1773
|
+
override_prices: z.record(z.number()).optional().describe("Price overrides by currency code (e.g. {\"gold\": 50})"),
|
|
1774
|
+
sort_order: z.number().optional().describe("New sort order"),
|
|
1775
|
+
available_from: z.string().optional().describe("ISO 8601 timestamp when the listing becomes available"),
|
|
1776
|
+
available_until: z.string().optional().describe("ISO 8601 timestamp when the listing expires"),
|
|
1777
|
+
max_purchases_per_user: z.number().optional().describe("Maximum number of times each user can purchase this listing")
|
|
1778
|
+
}, async ({ project_id, store_id, listing_id, override_prices, sort_order, available_from, available_until, max_purchases_per_user }) => {
|
|
1779
|
+
const payload = {};
|
|
1780
|
+
if (override_prices !== void 0) payload.override_prices = override_prices;
|
|
1781
|
+
if (sort_order !== void 0) payload.sort_order = sort_order;
|
|
1782
|
+
if (available_from !== void 0) payload.available_from = available_from;
|
|
1783
|
+
if (available_until !== void 0) payload.available_until = available_until;
|
|
1784
|
+
if (max_purchases_per_user !== void 0) payload.max_purchases_per_user = max_purchases_per_user;
|
|
1785
|
+
const result = await apiClient.patch(`/projects/${project_id}/stores/${store_id}/listings/${listing_id}`, payload);
|
|
1786
|
+
return { content: [{
|
|
1787
|
+
type: "text",
|
|
1788
|
+
text: JSON.stringify(result, null, 2)
|
|
1789
|
+
}] };
|
|
1790
|
+
});
|
|
1791
|
+
server.tool("amba_stores_delete_listing", "Remove a listing from a store. The underlying catalog item is unaffected.", {
|
|
1792
|
+
project_id: z.string().describe("The project ID"),
|
|
1793
|
+
store_id: z.string().describe("The store ID"),
|
|
1794
|
+
listing_id: z.string().describe("The listing ID")
|
|
1795
|
+
}, async ({ project_id, store_id, listing_id }) => {
|
|
1796
|
+
const result = await apiClient.delete(`/projects/${project_id}/stores/${store_id}/listings/${listing_id}`);
|
|
1797
|
+
return { content: [{
|
|
1798
|
+
type: "text",
|
|
1799
|
+
text: JSON.stringify(result, null, 2)
|
|
1800
|
+
}] };
|
|
1801
|
+
});
|
|
1802
|
+
server.tool("amba_get_catalog_item", "Fetch a single catalog item by ID, including its prices and any bundled content.", {
|
|
1803
|
+
project_id: z.string().describe("The project ID"),
|
|
1804
|
+
item_id: z.string().describe("The catalog item ID")
|
|
1805
|
+
}, async ({ project_id, item_id }) => {
|
|
1806
|
+
const result = await apiClient.get(`/projects/${project_id}/catalog/${item_id}`);
|
|
1807
|
+
return { content: [{
|
|
1808
|
+
type: "text",
|
|
1809
|
+
text: JSON.stringify(result, null, 2)
|
|
1810
|
+
}] };
|
|
1811
|
+
});
|
|
1812
|
+
server.tool("amba_update_catalog_item", "Partially update a catalog item. Only the supplied fields are touched.", {
|
|
1813
|
+
project_id: z.string().describe("The project ID"),
|
|
1814
|
+
item_id: z.string().describe("The catalog item ID"),
|
|
1815
|
+
name: z.string().optional().describe("Display name"),
|
|
1816
|
+
description: z.string().optional().describe("Item description"),
|
|
1817
|
+
icon_url: z.string().optional().describe("Item icon URL"),
|
|
1818
|
+
type: z.enum([
|
|
1819
|
+
"consumable",
|
|
1820
|
+
"durable",
|
|
1821
|
+
"currency_pack",
|
|
1822
|
+
"bundle"
|
|
1823
|
+
]).optional().describe("Item type"),
|
|
1824
|
+
metadata: z.record(z.unknown()).optional().describe("Custom metadata payload"),
|
|
1825
|
+
is_active: z.boolean().optional().describe("Whether the item is active")
|
|
1826
|
+
}, async ({ project_id, item_id, name, description, icon_url, type, metadata, is_active }) => {
|
|
1827
|
+
const payload = {};
|
|
1828
|
+
if (name !== void 0) payload.name = name;
|
|
1829
|
+
if (description !== void 0) payload.description = description;
|
|
1830
|
+
if (icon_url !== void 0) payload.icon_url = icon_url;
|
|
1831
|
+
if (type !== void 0) payload.type = type;
|
|
1832
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
1833
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
1834
|
+
const result = await apiClient.patch(`/projects/${project_id}/catalog/${item_id}`, payload);
|
|
1835
|
+
return { content: [{
|
|
1836
|
+
type: "text",
|
|
1837
|
+
text: JSON.stringify(result, null, 2)
|
|
1838
|
+
}] };
|
|
1839
|
+
});
|
|
1840
|
+
server.tool("amba_delete_catalog_item", "Delete a catalog item. Removes its prices and any bundle membership in the same transaction.", {
|
|
1841
|
+
project_id: z.string().describe("The project ID"),
|
|
1842
|
+
item_id: z.string().describe("The catalog item ID")
|
|
1843
|
+
}, async ({ project_id, item_id }) => {
|
|
1844
|
+
const result = await apiClient.delete(`/projects/${project_id}/catalog/${item_id}`);
|
|
1845
|
+
return { content: [{
|
|
1846
|
+
type: "text",
|
|
1847
|
+
text: JSON.stringify(result, null, 2)
|
|
1848
|
+
}] };
|
|
1849
|
+
});
|
|
1850
|
+
server.tool("amba_delete_catalog_price", "Remove a single currency price from a catalog item. Other prices on the item remain.", {
|
|
1851
|
+
project_id: z.string().describe("The project ID"),
|
|
1852
|
+
item_id: z.string().describe("The catalog item ID"),
|
|
1853
|
+
currency_code: z.string().describe("The currency code (e.g. \"USD\", \"GEMS\")")
|
|
1854
|
+
}, async ({ project_id, item_id, currency_code }) => {
|
|
1855
|
+
const result = await apiClient.delete(`/projects/${project_id}/catalog/${item_id}/prices/${encodeURIComponent(currency_code)}`);
|
|
1856
|
+
return { content: [{
|
|
1857
|
+
type: "text",
|
|
1858
|
+
text: JSON.stringify(result, null, 2)
|
|
1859
|
+
}] };
|
|
1860
|
+
});
|
|
1861
|
+
server.tool("amba_add_catalog_bundle_item", "Add a content item to a catalog item's bundle. The catalog item must be of type `bundle`.", {
|
|
1862
|
+
project_id: z.string().describe("The project ID"),
|
|
1863
|
+
item_id: z.string().describe("The catalog item ID (must be a bundle)"),
|
|
1864
|
+
content_item_id: z.string().describe("The content item ID to bundle in"),
|
|
1865
|
+
quantity: z.number().optional().describe("How many of this content item the bundle grants (default 1)")
|
|
1866
|
+
}, async ({ project_id, item_id, content_item_id, quantity }) => {
|
|
1867
|
+
const payload = { content_item_id };
|
|
1868
|
+
if (quantity !== void 0) payload.quantity = quantity;
|
|
1869
|
+
const result = await apiClient.post(`/projects/${project_id}/catalog/${item_id}/bundle`, payload);
|
|
1870
|
+
return { content: [{
|
|
1871
|
+
type: "text",
|
|
1872
|
+
text: JSON.stringify(result, null, 2)
|
|
1873
|
+
}] };
|
|
1874
|
+
});
|
|
1875
|
+
server.tool("amba_remove_catalog_bundle_item", "Remove a content item from a catalog item's bundle.", {
|
|
1876
|
+
project_id: z.string().describe("The project ID"),
|
|
1877
|
+
item_id: z.string().describe("The catalog item ID (must be a bundle)"),
|
|
1878
|
+
content_item_id: z.string().describe("The content item ID to remove from the bundle")
|
|
1879
|
+
}, async ({ project_id, item_id, content_item_id }) => {
|
|
1880
|
+
const result = await apiClient.delete(`/projects/${project_id}/catalog/${item_id}/bundle/${content_item_id}`);
|
|
1881
|
+
return { content: [{
|
|
1882
|
+
type: "text",
|
|
1883
|
+
text: JSON.stringify(result, null, 2)
|
|
1884
|
+
}] };
|
|
1885
|
+
});
|
|
1886
|
+
server.tool("amba_update_currency", "Partially update a virtual currency definition.", {
|
|
1887
|
+
project_id: z.string().describe("The project ID"),
|
|
1888
|
+
currency_id: z.string().describe("The currency ID"),
|
|
1889
|
+
name: z.string().optional().describe("Display name"),
|
|
1890
|
+
icon_url: z.string().optional().describe("Currency icon URL"),
|
|
1891
|
+
starting_balance: z.number().optional().describe("Starting balance granted on first user encounter"),
|
|
1892
|
+
max_balance: z.number().optional().describe("Maximum balance a user can hold"),
|
|
1893
|
+
is_active: z.boolean().optional().describe("Whether this currency is active")
|
|
1894
|
+
}, async ({ project_id, currency_id, name, icon_url, starting_balance, max_balance, is_active }) => {
|
|
1895
|
+
const payload = {};
|
|
1896
|
+
if (name !== void 0) payload.name = name;
|
|
1897
|
+
if (icon_url !== void 0) payload.icon_url = icon_url;
|
|
1898
|
+
if (starting_balance !== void 0) payload.starting_balance = starting_balance;
|
|
1899
|
+
if (max_balance !== void 0) payload.max_balance = max_balance;
|
|
1900
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
1901
|
+
const result = await apiClient.patch(`/projects/${project_id}/currencies/${currency_id}`, payload);
|
|
1902
|
+
return { content: [{
|
|
1903
|
+
type: "text",
|
|
1904
|
+
text: JSON.stringify(result, null, 2)
|
|
1905
|
+
}] };
|
|
1906
|
+
});
|
|
1907
|
+
server.tool("amba_delete_currency", "Delete a virtual currency definition. User balances and transaction history are NOT auto-cleaned — handle that explicitly if needed.", {
|
|
1908
|
+
project_id: z.string().describe("The project ID"),
|
|
1909
|
+
currency_id: z.string().describe("The currency ID")
|
|
1910
|
+
}, async ({ project_id, currency_id }) => {
|
|
1911
|
+
const result = await apiClient.delete(`/projects/${project_id}/currencies/${currency_id}`);
|
|
1912
|
+
return { content: [{
|
|
1913
|
+
type: "text",
|
|
1914
|
+
text: JSON.stringify(result, null, 2)
|
|
1915
|
+
}] };
|
|
1916
|
+
});
|
|
1917
|
+
server.tool("amba_get_currency_transactions", "List currency transactions for a single user, newest first. Useful for inspecting a user's grant/spend history when investigating support requests or fraud.", {
|
|
1918
|
+
project_id: z.string().describe("The project ID"),
|
|
1919
|
+
user_id: z.string().describe("The app user ID"),
|
|
1920
|
+
currency_id: z.string().optional().describe("Filter to one currency"),
|
|
1921
|
+
limit: z.number().optional().describe("Page size"),
|
|
1922
|
+
offset: z.number().optional().describe("Offset for pagination")
|
|
1923
|
+
}, async ({ project_id, user_id, currency_id, limit, offset }) => {
|
|
1924
|
+
const query = {};
|
|
1925
|
+
if (currency_id !== void 0) query.currency_id = currency_id;
|
|
1926
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
1927
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
1928
|
+
const result = await apiClient.get(`/projects/${project_id}/currencies/transactions/${user_id}`, query);
|
|
1929
|
+
return { content: [{
|
|
1930
|
+
type: "text",
|
|
1931
|
+
text: JSON.stringify(result, null, 2)
|
|
1932
|
+
}] };
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
//#endregion
|
|
1936
|
+
//#region src/tools/leaderboards.ts
|
|
1937
|
+
function registerTools$5(server, apiClient) {
|
|
1938
|
+
server.tool("amba_create_leaderboard", "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.", {
|
|
1939
|
+
project_id: z.string().describe("The project ID"),
|
|
1940
|
+
name: z.string().describe("Leaderboard name (e.g. \"Top XP Earners\", \"Weekly Streak Leaders\")"),
|
|
1941
|
+
metric: z.enum([
|
|
1942
|
+
"xp",
|
|
1943
|
+
"streak",
|
|
1944
|
+
"custom"
|
|
1945
|
+
]).describe("What metric to rank by: xp (total XP), streak (longest streak), custom (custom event count)"),
|
|
1946
|
+
period: z.enum([
|
|
1947
|
+
"all_time",
|
|
1948
|
+
"daily",
|
|
1949
|
+
"weekly",
|
|
1950
|
+
"monthly"
|
|
1951
|
+
]).optional().describe("Time period for the leaderboard. Defaults to \"all_time\"."),
|
|
1952
|
+
max_entries: z.number().optional().describe("Maximum number of entries to keep on the leaderboard (default 100)"),
|
|
1953
|
+
custom_event: z.string().optional().describe("Event name to count for custom metric leaderboards")
|
|
1954
|
+
}, async ({ project_id, name, metric, period, max_entries, custom_event }) => {
|
|
1955
|
+
const payload = {
|
|
1956
|
+
name,
|
|
1957
|
+
metric
|
|
1958
|
+
};
|
|
1959
|
+
if (period !== void 0) payload.period = period;
|
|
1960
|
+
if (max_entries !== void 0) payload.max_entries = max_entries;
|
|
1961
|
+
if (custom_event !== void 0) payload.custom_event = custom_event;
|
|
1962
|
+
const result = await apiClient.post(`/projects/${project_id}/leaderboards`, payload);
|
|
1963
|
+
return { content: [{
|
|
1964
|
+
type: "text",
|
|
1965
|
+
text: JSON.stringify(result, null, 2)
|
|
1966
|
+
}] };
|
|
1967
|
+
});
|
|
1968
|
+
server.tool("amba_get_leaderboard", "Get leaderboard entries with user display names and avatars. Returns ranked entries with scores.", {
|
|
1969
|
+
project_id: z.string().describe("The project ID"),
|
|
1970
|
+
leaderboard_id: z.string().describe("The leaderboard definition ID"),
|
|
1971
|
+
limit: z.number().optional().describe("Number of entries to return (default 50)"),
|
|
1972
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
1973
|
+
}, async ({ project_id, leaderboard_id, limit, offset }) => {
|
|
1974
|
+
const query = {};
|
|
1975
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
1976
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
1977
|
+
const result = await apiClient.get(`/projects/${project_id}/leaderboards/${leaderboard_id}/entries`, query);
|
|
1978
|
+
return { content: [{
|
|
1979
|
+
type: "text",
|
|
1980
|
+
text: JSON.stringify(result, null, 2)
|
|
1981
|
+
}] };
|
|
1982
|
+
});
|
|
1983
|
+
server.tool("amba_list_leaderboards", "List all leaderboard definitions for a project. Returns name, metric, period, and max_entries per leaderboard. (Use `amba_get_leaderboard` to fetch ranked entries for a specific leaderboard.)", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
1984
|
+
const result = await apiClient.get(`/projects/${project_id}/leaderboards`);
|
|
1985
|
+
return { content: [{
|
|
1986
|
+
type: "text",
|
|
1987
|
+
text: JSON.stringify(result, null, 2)
|
|
1988
|
+
}] };
|
|
1989
|
+
});
|
|
1990
|
+
server.tool("amba_get_leaderboard_definition", "Fetch a single leaderboard definition by ID — its metric, period, and configuration. (Use `amba_get_leaderboard` for ranked entries instead.)", {
|
|
1991
|
+
project_id: z.string().describe("The project ID"),
|
|
1992
|
+
leaderboard_id: z.string().describe("The leaderboard definition ID")
|
|
1993
|
+
}, async ({ project_id, leaderboard_id }) => {
|
|
1994
|
+
const result = await apiClient.get(`/projects/${project_id}/leaderboards/${leaderboard_id}`);
|
|
1995
|
+
return { content: [{
|
|
1996
|
+
type: "text",
|
|
1997
|
+
text: JSON.stringify(result, null, 2)
|
|
1998
|
+
}] };
|
|
1999
|
+
});
|
|
2000
|
+
server.tool("amba_update_leaderboard", "Partially update a leaderboard definition. Only the supplied fields are touched.", {
|
|
2001
|
+
project_id: z.string().describe("The project ID"),
|
|
2002
|
+
leaderboard_id: z.string().describe("The leaderboard definition ID"),
|
|
2003
|
+
name: z.string().optional().describe("New display name"),
|
|
2004
|
+
metric: z.enum([
|
|
2005
|
+
"xp",
|
|
2006
|
+
"streak",
|
|
2007
|
+
"custom"
|
|
2008
|
+
]).optional().describe("New ranking metric"),
|
|
2009
|
+
period: z.enum([
|
|
2010
|
+
"all_time",
|
|
2011
|
+
"daily",
|
|
2012
|
+
"weekly",
|
|
2013
|
+
"monthly"
|
|
2014
|
+
]).optional().describe("New time period"),
|
|
2015
|
+
max_entries: z.number().optional().describe("New max entries cap"),
|
|
2016
|
+
custom_event: z.string().optional().describe("Event name to count for custom metric leaderboards")
|
|
2017
|
+
}, async ({ project_id, leaderboard_id, name, metric, period, max_entries, custom_event }) => {
|
|
2018
|
+
const payload = {};
|
|
2019
|
+
if (name !== void 0) payload.name = name;
|
|
2020
|
+
if (metric !== void 0) payload.metric = metric;
|
|
2021
|
+
if (period !== void 0) payload.period = period;
|
|
2022
|
+
if (max_entries !== void 0) payload.max_entries = max_entries;
|
|
2023
|
+
if (custom_event !== void 0) payload.custom_event = custom_event;
|
|
2024
|
+
const result = await apiClient.patch(`/projects/${project_id}/leaderboards/${leaderboard_id}`, payload);
|
|
2025
|
+
return { content: [{
|
|
2026
|
+
type: "text",
|
|
2027
|
+
text: JSON.stringify(result, null, 2)
|
|
2028
|
+
}] };
|
|
2029
|
+
});
|
|
2030
|
+
server.tool("amba_delete_leaderboard", "Delete a leaderboard definition. Existing entries are cleared as part of the delete.", {
|
|
2031
|
+
project_id: z.string().describe("The project ID"),
|
|
2032
|
+
leaderboard_id: z.string().describe("The leaderboard definition ID")
|
|
2033
|
+
}, async ({ project_id, leaderboard_id }) => {
|
|
2034
|
+
const result = await apiClient.delete(`/projects/${project_id}/leaderboards/${leaderboard_id}`);
|
|
2035
|
+
return { content: [{
|
|
2036
|
+
type: "text",
|
|
2037
|
+
text: JSON.stringify(result, null, 2)
|
|
2038
|
+
}] };
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
//#endregion
|
|
2042
|
+
//#region src/tools/platform.ts
|
|
2043
|
+
function registerTools$4(server, apiClient) {
|
|
2044
|
+
server.tool("amba_create_onboarding_flow", "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).", {
|
|
2045
|
+
project_id: z.string().describe("The project ID"),
|
|
2046
|
+
name: z.string().describe("Flow name (e.g. \"Welcome Flow\", \"Premium Onboarding\")"),
|
|
2047
|
+
steps: z.array(z.object({
|
|
2048
|
+
id: z.string().describe("Unique step identifier"),
|
|
2049
|
+
title: z.string().describe("Step title"),
|
|
2050
|
+
description: z.string().describe("Step description"),
|
|
2051
|
+
type: z.string().describe("Step type (e.g. \"welcome\", \"permission\", \"feature_tour\", \"personalization\")"),
|
|
2052
|
+
config: z.record(z.unknown()).optional().describe("Step-specific configuration")
|
|
2053
|
+
})).describe("Ordered array of onboarding steps"),
|
|
2054
|
+
is_active: z.boolean().optional().describe("Whether the flow is active. Defaults to true.")
|
|
2055
|
+
}, async ({ project_id, name, steps, is_active }) => {
|
|
2056
|
+
const payload = {
|
|
2057
|
+
name,
|
|
2058
|
+
steps
|
|
2059
|
+
};
|
|
2060
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
2061
|
+
const result = await apiClient.post(`/projects/${project_id}/onboarding/flows`, payload);
|
|
2062
|
+
return { content: [{
|
|
2063
|
+
type: "text",
|
|
2064
|
+
text: JSON.stringify(result, null, 2)
|
|
2065
|
+
}] };
|
|
2066
|
+
});
|
|
2067
|
+
server.tool("amba_get_onboarding_stats", "Get onboarding completion statistics for a project. Shows how many users started, completed, and skipped each flow.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
2068
|
+
const result = await apiClient.get(`/projects/${project_id}/onboarding/stats`);
|
|
2069
|
+
return { content: [{
|
|
2070
|
+
type: "text",
|
|
2071
|
+
text: JSON.stringify(result, null, 2)
|
|
2072
|
+
}] };
|
|
2073
|
+
});
|
|
2074
|
+
server.tool("amba_onboarding_list", "List all onboarding flows for a project. Returns flow id, name, steps config, and is_active.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
2075
|
+
const result = await apiClient.get(`/projects/${project_id}/onboarding/flows`);
|
|
2076
|
+
return { content: [{
|
|
2077
|
+
type: "text",
|
|
2078
|
+
text: JSON.stringify(result, null, 2)
|
|
2079
|
+
}] };
|
|
2080
|
+
});
|
|
2081
|
+
server.tool("amba_onboarding_get", "Get a single onboarding flow by id including its ordered steps.", {
|
|
2082
|
+
project_id: z.string().describe("The project ID"),
|
|
2083
|
+
flow_id: z.string().describe("The onboarding flow ID")
|
|
2084
|
+
}, async ({ project_id, flow_id }) => {
|
|
2085
|
+
const result = await apiClient.get(`/projects/${project_id}/onboarding/flows/${flow_id}`);
|
|
2086
|
+
return { content: [{
|
|
2087
|
+
type: "text",
|
|
2088
|
+
text: JSON.stringify(result, null, 2)
|
|
2089
|
+
}] };
|
|
2090
|
+
});
|
|
2091
|
+
server.tool("amba_onboarding_update", "Update an onboarding flow. Any omitted fields are left unchanged. Pass `steps` to replace the step list.", {
|
|
2092
|
+
project_id: z.string().describe("The project ID"),
|
|
2093
|
+
flow_id: z.string().describe("The onboarding flow ID"),
|
|
2094
|
+
name: z.string().optional().describe("New flow name"),
|
|
2095
|
+
steps: z.array(z.record(z.unknown())).optional().describe("Replacement ordered step list"),
|
|
2096
|
+
is_active: z.boolean().optional().describe("Toggle whether the flow is active")
|
|
2097
|
+
}, async ({ project_id, flow_id, name, steps, is_active }) => {
|
|
2098
|
+
const payload = {};
|
|
2099
|
+
if (name !== void 0) payload.name = name;
|
|
2100
|
+
if (steps !== void 0) payload.steps = steps;
|
|
2101
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
2102
|
+
const result = await apiClient.patch(`/projects/${project_id}/onboarding/flows/${flow_id}`, payload);
|
|
2103
|
+
return { content: [{
|
|
2104
|
+
type: "text",
|
|
2105
|
+
text: JSON.stringify(result, null, 2)
|
|
2106
|
+
}] };
|
|
2107
|
+
});
|
|
2108
|
+
server.tool("amba_onboarding_delete", "Delete an onboarding flow. Existing user onboarding records referencing this flow are preserved; no new users will enter the deleted flow.", {
|
|
2109
|
+
project_id: z.string().describe("The project ID"),
|
|
2110
|
+
flow_id: z.string().describe("The onboarding flow ID")
|
|
2111
|
+
}, async ({ project_id, flow_id }) => {
|
|
2112
|
+
const result = await apiClient.delete(`/projects/${project_id}/onboarding/flows/${flow_id}`);
|
|
2113
|
+
return { content: [{
|
|
2114
|
+
type: "text",
|
|
2115
|
+
text: JSON.stringify(result, null, 2)
|
|
2116
|
+
}] };
|
|
2117
|
+
});
|
|
2118
|
+
server.tool("amba_create_role", "Create a role with specific permissions for a project. Roles define what users can do (e.g. admin, moderator, premium). Permissions are string identifiers your app checks at runtime.", {
|
|
2119
|
+
project_id: z.string().describe("The project ID"),
|
|
2120
|
+
name: z.string().describe("Role name (e.g. \"admin\", \"moderator\", \"premium\")"),
|
|
2121
|
+
description: z.string().optional().describe("Human-readable description of the role"),
|
|
2122
|
+
permissions: z.array(z.string()).describe("Array of permission strings (e.g. [\"content:write\", \"users:read\", \"moderation:manage\"])")
|
|
2123
|
+
}, async ({ project_id, name, description, permissions }) => {
|
|
2124
|
+
const payload = {
|
|
2125
|
+
name,
|
|
2126
|
+
permissions
|
|
2127
|
+
};
|
|
2128
|
+
if (description !== void 0) payload.description = description;
|
|
2129
|
+
const result = await apiClient.post(`/projects/${project_id}/roles`, payload);
|
|
2130
|
+
return { content: [{
|
|
2131
|
+
type: "text",
|
|
2132
|
+
text: JSON.stringify(result, null, 2)
|
|
2133
|
+
}] };
|
|
2134
|
+
});
|
|
2135
|
+
server.tool("amba_list_roles", "List all roles defined for a project.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
2136
|
+
const result = await apiClient.get(`/projects/${project_id}/roles`);
|
|
2137
|
+
return { content: [{
|
|
2138
|
+
type: "text",
|
|
2139
|
+
text: JSON.stringify(result, null, 2)
|
|
2140
|
+
}] };
|
|
2141
|
+
});
|
|
2142
|
+
server.tool("amba_assign_role", "Assign a role to an app user. The user will inherit all permissions defined on the role.", {
|
|
2143
|
+
project_id: z.string().describe("The project ID"),
|
|
2144
|
+
app_user_id: z.string().describe("The app user ID to assign the role to"),
|
|
2145
|
+
role_id: z.string().describe("The role ID to assign")
|
|
2146
|
+
}, async ({ project_id, app_user_id, role_id }) => {
|
|
2147
|
+
const result = await apiClient.post(`/projects/${project_id}/roles/assign`, {
|
|
2148
|
+
app_user_id,
|
|
2149
|
+
role_id
|
|
2150
|
+
});
|
|
2151
|
+
return { content: [{
|
|
2152
|
+
type: "text",
|
|
2153
|
+
text: JSON.stringify(result, null, 2)
|
|
2154
|
+
}] };
|
|
2155
|
+
});
|
|
2156
|
+
server.tool("amba_create_tracked_link", "Create a tracked deep link for a project. Tracked links have a short slug, destination URL, and optional campaign metadata. Click analytics are recorded automatically.", {
|
|
2157
|
+
project_id: z.string().describe("The project ID"),
|
|
2158
|
+
slug: z.string().describe("Short URL slug (e.g. \"summer-promo\", \"onboarding-cta\")"),
|
|
2159
|
+
destination_url: z.string().describe("The destination URL the link resolves to"),
|
|
2160
|
+
metadata: z.record(z.unknown()).optional().describe("Campaign metadata (e.g. { campaign: \"summer\", source: \"email\", medium: \"newsletter\" })")
|
|
2161
|
+
}, async ({ project_id, slug, destination_url, metadata }) => {
|
|
2162
|
+
const payload = {
|
|
2163
|
+
slug,
|
|
2164
|
+
destination_url
|
|
2165
|
+
};
|
|
2166
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
2167
|
+
const result = await apiClient.post(`/projects/${project_id}/deep-links/tracked`, payload);
|
|
2168
|
+
return { content: [{
|
|
2169
|
+
type: "text",
|
|
2170
|
+
text: JSON.stringify(result, null, 2)
|
|
2171
|
+
}] };
|
|
2172
|
+
});
|
|
2173
|
+
server.tool("amba_get_link_stats", "Get click analytics for a tracked link including total clicks, unique users, and clicks by platform.", {
|
|
2174
|
+
project_id: z.string().describe("The project ID"),
|
|
2175
|
+
link_id: z.string().describe("The tracked link ID")
|
|
2176
|
+
}, async ({ project_id, link_id }) => {
|
|
2177
|
+
const result = await apiClient.get(`/projects/${project_id}/deep-links/tracked/${link_id}/stats`);
|
|
2178
|
+
return { content: [{
|
|
2179
|
+
type: "text",
|
|
2180
|
+
text: JSON.stringify(result, null, 2)
|
|
2181
|
+
}] };
|
|
2182
|
+
});
|
|
2183
|
+
server.tool("amba_deeplinks_list", "List tracked deep links for a project, newest first. Supports pagination.", {
|
|
2184
|
+
project_id: z.string().describe("The project ID"),
|
|
2185
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
2186
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
2187
|
+
}, async ({ project_id, limit, offset }) => {
|
|
2188
|
+
const query = {};
|
|
2189
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2190
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2191
|
+
const result = await apiClient.get(`/projects/${project_id}/deep-links/tracked`, query);
|
|
2192
|
+
return { content: [{
|
|
2193
|
+
type: "text",
|
|
2194
|
+
text: JSON.stringify(result, null, 2)
|
|
2195
|
+
}] };
|
|
2196
|
+
});
|
|
2197
|
+
server.tool("amba_deeplinks_delete", "Delete a tracked deep link. Historical click rows for the link are preserved.", {
|
|
2198
|
+
project_id: z.string().describe("The project ID"),
|
|
2199
|
+
link_id: z.string().describe("The tracked link ID")
|
|
2200
|
+
}, async ({ project_id, link_id }) => {
|
|
2201
|
+
const result = await apiClient.delete(`/projects/${project_id}/deep-links/tracked/${link_id}`);
|
|
2202
|
+
return { content: [{
|
|
2203
|
+
type: "text",
|
|
2204
|
+
text: JSON.stringify(result, null, 2)
|
|
2205
|
+
}] };
|
|
2206
|
+
});
|
|
2207
|
+
server.tool("amba_deeplinks_get_config", "Get the deep-link configuration (URL scheme, universal link domain, iOS/Android identifiers, fallback URL) for a project.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
2208
|
+
const result = await apiClient.get(`/projects/${project_id}/deep-links/config`);
|
|
2209
|
+
return { content: [{
|
|
2210
|
+
type: "text",
|
|
2211
|
+
text: JSON.stringify(result, null, 2)
|
|
2212
|
+
}] };
|
|
2213
|
+
});
|
|
2214
|
+
server.tool("amba_deeplinks_set_config", "Upsert the deep-link configuration (URL scheme, universal link domain, iOS/Android identifiers, fallback URL) for a project.", {
|
|
2215
|
+
project_id: z.string().describe("The project ID"),
|
|
2216
|
+
url_scheme: z.string().optional().describe("Custom URL scheme (e.g. \"myapp\")"),
|
|
2217
|
+
universal_link_domain: z.string().optional().describe("Universal link domain (e.g. \"links.example.com\")"),
|
|
2218
|
+
android_package_name: z.string().optional().describe("Android package name"),
|
|
2219
|
+
ios_bundle_id: z.string().optional().describe("iOS bundle identifier"),
|
|
2220
|
+
fallback_url: z.string().optional().describe("Fallback URL for users without the app installed")
|
|
2221
|
+
}, async ({ project_id, url_scheme, universal_link_domain, android_package_name, ios_bundle_id, fallback_url }) => {
|
|
2222
|
+
const payload = {};
|
|
2223
|
+
if (url_scheme !== void 0) payload.url_scheme = url_scheme;
|
|
2224
|
+
if (universal_link_domain !== void 0) payload.universal_link_domain = universal_link_domain;
|
|
2225
|
+
if (android_package_name !== void 0) payload.android_package_name = android_package_name;
|
|
2226
|
+
if (ios_bundle_id !== void 0) payload.ios_bundle_id = ios_bundle_id;
|
|
2227
|
+
if (fallback_url !== void 0) payload.fallback_url = fallback_url;
|
|
2228
|
+
const result = await apiClient.put(`/projects/${project_id}/deep-links/config`, payload);
|
|
2229
|
+
return { content: [{
|
|
2230
|
+
type: "text",
|
|
2231
|
+
text: JSON.stringify(result, null, 2)
|
|
2232
|
+
}] };
|
|
2233
|
+
});
|
|
2234
|
+
server.tool("amba_upload_media", "Register a media asset and get a signed upload URL. After calling this, upload the file to the returned upload_url.", {
|
|
2235
|
+
project_id: z.string().describe("The project ID"),
|
|
2236
|
+
filename: z.string().describe("Original filename (e.g. \"hero-banner.png\")"),
|
|
2237
|
+
mime_type: z.string().describe("MIME type (e.g. \"image/png\", \"video/mp4\")"),
|
|
2238
|
+
size_bytes: z.number().optional().describe("File size in bytes"),
|
|
2239
|
+
folder_id: z.string().optional().describe("ID of the folder to place the asset in"),
|
|
2240
|
+
alt_text: z.string().optional().describe("Alt text for accessibility"),
|
|
2241
|
+
metadata: z.record(z.unknown()).optional().describe("Custom metadata")
|
|
2242
|
+
}, async ({ project_id, filename, mime_type, size_bytes, folder_id, alt_text, metadata }) => {
|
|
2243
|
+
const payload = {
|
|
2244
|
+
filename,
|
|
2245
|
+
mime_type
|
|
2246
|
+
};
|
|
2247
|
+
if (size_bytes !== void 0) payload.size_bytes = size_bytes;
|
|
2248
|
+
if (folder_id !== void 0) payload.folder_id = folder_id;
|
|
2249
|
+
if (alt_text !== void 0) payload.alt_text = alt_text;
|
|
2250
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
2251
|
+
const result = await apiClient.post(`/projects/${project_id}/media`, payload);
|
|
2252
|
+
return { content: [{
|
|
2253
|
+
type: "text",
|
|
2254
|
+
text: JSON.stringify(result, null, 2)
|
|
2255
|
+
}] };
|
|
2256
|
+
});
|
|
2257
|
+
server.tool("amba_list_media", "List media assets for a project. Supports pagination and filtering by folder.", {
|
|
2258
|
+
project_id: z.string().describe("The project ID"),
|
|
2259
|
+
folder_id: z.string().optional().describe("Filter by folder ID"),
|
|
2260
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
2261
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
2262
|
+
}, async ({ project_id, folder_id, limit, offset }) => {
|
|
2263
|
+
const query = {};
|
|
2264
|
+
if (folder_id) query.folder_id = folder_id;
|
|
2265
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2266
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2267
|
+
const result = await apiClient.get(`/projects/${project_id}/media`, query);
|
|
2268
|
+
return { content: [{
|
|
2269
|
+
type: "text",
|
|
2270
|
+
text: JSON.stringify(result, null, 2)
|
|
2271
|
+
}] };
|
|
2272
|
+
});
|
|
2273
|
+
server.tool("amba_media_delete", "Delete a media asset. This removes the DB row; the underlying object in storage is garbage-collected separately.", {
|
|
2274
|
+
project_id: z.string().describe("The project ID"),
|
|
2275
|
+
asset_id: z.string().describe("The media asset ID")
|
|
2276
|
+
}, async ({ project_id, asset_id }) => {
|
|
2277
|
+
const result = await apiClient.delete(`/projects/${project_id}/media/${asset_id}`);
|
|
2278
|
+
return { content: [{
|
|
2279
|
+
type: "text",
|
|
2280
|
+
text: JSON.stringify(result, null, 2)
|
|
2281
|
+
}] };
|
|
2282
|
+
});
|
|
2283
|
+
server.tool("amba_media_create_folder", "Create a media folder for organizing assets. Folders can be nested via `parent_id`.", {
|
|
2284
|
+
project_id: z.string().describe("The project ID"),
|
|
2285
|
+
name: z.string().describe("Folder name"),
|
|
2286
|
+
parent_id: z.string().optional().describe("Parent folder ID. Omit for a top-level folder.")
|
|
2287
|
+
}, async ({ project_id, name, parent_id }) => {
|
|
2288
|
+
const payload = { name };
|
|
2289
|
+
if (parent_id !== void 0) payload.parent_id = parent_id;
|
|
2290
|
+
const result = await apiClient.post(`/projects/${project_id}/media/folders`, payload);
|
|
2291
|
+
return { content: [{
|
|
2292
|
+
type: "text",
|
|
2293
|
+
text: JSON.stringify(result, null, 2)
|
|
2294
|
+
}] };
|
|
2295
|
+
});
|
|
2296
|
+
server.tool("amba_media_list_folders", "List media folders for a project. Pass `parent_id` to list children of a specific folder; omit to list top-level folders.", {
|
|
2297
|
+
project_id: z.string().describe("The project ID"),
|
|
2298
|
+
parent_id: z.string().optional().describe("Parent folder ID. Omit for top-level folders.")
|
|
2299
|
+
}, async ({ project_id, parent_id }) => {
|
|
2300
|
+
const query = {};
|
|
2301
|
+
if (parent_id) query.parent_id = parent_id;
|
|
2302
|
+
const result = await apiClient.get(`/projects/${project_id}/media/folders`, query);
|
|
2303
|
+
return { content: [{
|
|
2304
|
+
type: "text",
|
|
2305
|
+
text: JSON.stringify(result, null, 2)
|
|
2306
|
+
}] };
|
|
2307
|
+
});
|
|
2308
|
+
server.tool("amba_media_delete_folder", "Delete a media folder. Assets inside the folder are not deleted; their folder_id is left dangling and should be reassigned before calling this tool.", {
|
|
2309
|
+
project_id: z.string().describe("The project ID"),
|
|
2310
|
+
folder_id: z.string().describe("The media folder ID")
|
|
2311
|
+
}, async ({ project_id, folder_id }) => {
|
|
2312
|
+
const result = await apiClient.delete(`/projects/${project_id}/media/folders/${folder_id}`);
|
|
2313
|
+
return { content: [{
|
|
2314
|
+
type: "text",
|
|
2315
|
+
text: JSON.stringify(result, null, 2)
|
|
2316
|
+
}] };
|
|
2317
|
+
});
|
|
2318
|
+
server.tool("amba_get_moderation_queue", "Get the content moderation queue for a project. Shows reported content pending review.", {
|
|
2319
|
+
project_id: z.string().describe("The project ID"),
|
|
2320
|
+
status: z.enum([
|
|
2321
|
+
"pending",
|
|
2322
|
+
"approved",
|
|
2323
|
+
"rejected",
|
|
2324
|
+
"escalated"
|
|
2325
|
+
]).optional().describe("Filter by status. Defaults to \"pending\"."),
|
|
2326
|
+
limit: z.number().optional().describe("Max results to return (default 50)")
|
|
2327
|
+
}, async ({ project_id, status, limit }) => {
|
|
2328
|
+
const query = {};
|
|
2329
|
+
if (status) query.status = status;
|
|
2330
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2331
|
+
const result = await apiClient.get(`/projects/${project_id}/moderation/queue`, query);
|
|
2332
|
+
return { content: [{
|
|
2333
|
+
type: "text",
|
|
2334
|
+
text: JSON.stringify(result, null, 2)
|
|
2335
|
+
}] };
|
|
2336
|
+
});
|
|
2337
|
+
server.tool("amba_configure_moderation", "Create a moderation rule for a project. Rules can auto-filter content based on keywords, regex patterns, or auto-approve trusted users.", {
|
|
2338
|
+
project_id: z.string().describe("The project ID"),
|
|
2339
|
+
name: z.string().describe("Rule name (e.g. \"Profanity Filter\", \"Spam Detection\")"),
|
|
2340
|
+
rule_type: z.enum([
|
|
2341
|
+
"keyword_filter",
|
|
2342
|
+
"regex",
|
|
2343
|
+
"auto_approve_trusted"
|
|
2344
|
+
]).describe("Type of moderation rule"),
|
|
2345
|
+
config: z.record(z.unknown()).describe("Rule configuration (e.g. { keywords: [\"spam\", \"scam\"] } for keyword_filter, { pattern: \"\\\\b(buy now)\\\\b\" } for regex)"),
|
|
2346
|
+
is_active: z.boolean().optional().describe("Whether the rule is active. Defaults to true.")
|
|
2347
|
+
}, async ({ project_id, name, rule_type, config, is_active }) => {
|
|
2348
|
+
const payload = {
|
|
2349
|
+
name,
|
|
2350
|
+
rule_type,
|
|
2351
|
+
config
|
|
2352
|
+
};
|
|
2353
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
2354
|
+
const result = await apiClient.post(`/projects/${project_id}/moderation/rules`, payload);
|
|
2355
|
+
return { content: [{
|
|
2356
|
+
type: "text",
|
|
2357
|
+
text: JSON.stringify(result, null, 2)
|
|
2358
|
+
}] };
|
|
2359
|
+
});
|
|
2360
|
+
server.tool("amba_moderation_queue_list", "List moderation queue items for a project, filtered by status. Pagination via limit+offset.", {
|
|
2361
|
+
project_id: z.string().describe("The project ID"),
|
|
2362
|
+
status: z.enum([
|
|
2363
|
+
"pending",
|
|
2364
|
+
"approved",
|
|
2365
|
+
"rejected",
|
|
2366
|
+
"escalated"
|
|
2367
|
+
]).optional().describe("Queue status filter. Defaults to \"pending\"."),
|
|
2368
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
2369
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
2370
|
+
}, async ({ project_id, status, limit, offset }) => {
|
|
2371
|
+
const query = {};
|
|
2372
|
+
if (status) query.status = status;
|
|
2373
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2374
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2375
|
+
const result = await apiClient.get(`/projects/${project_id}/moderation/queue`, query);
|
|
2376
|
+
return { content: [{
|
|
2377
|
+
type: "text",
|
|
2378
|
+
text: JSON.stringify(result, null, 2)
|
|
2379
|
+
}] };
|
|
2380
|
+
});
|
|
2381
|
+
server.tool("amba_moderation_queue_approve", "Approve a moderation-queue item. The item transitions to status=\"approved\" and the reported content stays visible.", {
|
|
2382
|
+
project_id: z.string().describe("The project ID"),
|
|
2383
|
+
item_id: z.string().describe("The moderation queue item ID")
|
|
2384
|
+
}, async ({ project_id, item_id }) => {
|
|
2385
|
+
const result = await apiClient.post(`/projects/${project_id}/moderation/queue/${item_id}/approve`);
|
|
2386
|
+
return { content: [{
|
|
2387
|
+
type: "text",
|
|
2388
|
+
text: JSON.stringify(result, null, 2)
|
|
2389
|
+
}] };
|
|
2390
|
+
});
|
|
2391
|
+
server.tool("amba_moderation_queue_reject", "Reject a moderation-queue item. The item transitions to status=\"rejected\" — your app should treat the reported content as removed.", {
|
|
2392
|
+
project_id: z.string().describe("The project ID"),
|
|
2393
|
+
item_id: z.string().describe("The moderation queue item ID")
|
|
2394
|
+
}, async ({ project_id, item_id }) => {
|
|
2395
|
+
const result = await apiClient.post(`/projects/${project_id}/moderation/queue/${item_id}/reject`);
|
|
2396
|
+
return { content: [{
|
|
2397
|
+
type: "text",
|
|
2398
|
+
text: JSON.stringify(result, null, 2)
|
|
2399
|
+
}] };
|
|
2400
|
+
});
|
|
2401
|
+
server.tool("amba_moderation_queue_escalate", "Escalate a moderation-queue item for senior review. Transitions the item to status=\"escalated\".", {
|
|
2402
|
+
project_id: z.string().describe("The project ID"),
|
|
2403
|
+
item_id: z.string().describe("The moderation queue item ID")
|
|
2404
|
+
}, async ({ project_id, item_id }) => {
|
|
2405
|
+
const result = await apiClient.post(`/projects/${project_id}/moderation/queue/${item_id}/escalate`);
|
|
2406
|
+
return { content: [{
|
|
2407
|
+
type: "text",
|
|
2408
|
+
text: JSON.stringify(result, null, 2)
|
|
2409
|
+
}] };
|
|
2410
|
+
});
|
|
2411
|
+
server.tool("amba_roles_patch", "Update a role's name, description, or permission list. Only the supplied fields are changed.", {
|
|
2412
|
+
project_id: z.string().describe("The project ID"),
|
|
2413
|
+
role_id: z.string().describe("The role ID"),
|
|
2414
|
+
name: z.string().optional().describe("New role name"),
|
|
2415
|
+
description: z.string().optional().describe("New description"),
|
|
2416
|
+
permissions: z.array(z.string()).optional().describe("Replacement array of permission strings")
|
|
2417
|
+
}, async ({ project_id, role_id, name, description, permissions }) => {
|
|
2418
|
+
const payload = {};
|
|
2419
|
+
if (name !== void 0) payload.name = name;
|
|
2420
|
+
if (description !== void 0) payload.description = description;
|
|
2421
|
+
if (permissions !== void 0) payload.permissions = permissions;
|
|
2422
|
+
const result = await apiClient.patch(`/projects/${project_id}/roles/${role_id}`, payload);
|
|
2423
|
+
return { content: [{
|
|
2424
|
+
type: "text",
|
|
2425
|
+
text: JSON.stringify(result, null, 2)
|
|
2426
|
+
}] };
|
|
2427
|
+
});
|
|
2428
|
+
server.tool("amba_roles_delete", "Delete a role. Users currently assigned to the role lose that assignment via cascade.", {
|
|
2429
|
+
project_id: z.string().describe("The project ID"),
|
|
2430
|
+
role_id: z.string().describe("The role ID")
|
|
2431
|
+
}, async ({ project_id, role_id }) => {
|
|
2432
|
+
const result = await apiClient.delete(`/projects/${project_id}/roles/${role_id}`);
|
|
2433
|
+
return { content: [{
|
|
2434
|
+
type: "text",
|
|
2435
|
+
text: JSON.stringify(result, null, 2)
|
|
2436
|
+
}] };
|
|
2437
|
+
});
|
|
2438
|
+
server.tool("amba_roles_revoke", "Revoke a role from an app user. The user loses the permissions that were only granted by this role.", {
|
|
2439
|
+
project_id: z.string().describe("The project ID"),
|
|
2440
|
+
app_user_id: z.string().describe("The app user ID"),
|
|
2441
|
+
role_id: z.string().describe("The role ID to revoke")
|
|
2442
|
+
}, async ({ project_id, app_user_id, role_id }) => {
|
|
2443
|
+
const result = await apiClient.post(`/projects/${project_id}/roles/revoke`, {
|
|
2444
|
+
app_user_id,
|
|
2445
|
+
role_id
|
|
2446
|
+
});
|
|
2447
|
+
return { content: [{
|
|
2448
|
+
type: "text",
|
|
2449
|
+
text: JSON.stringify(result, null, 2)
|
|
2450
|
+
}] };
|
|
2451
|
+
});
|
|
2452
|
+
server.tool("amba_roles_list_users", "List all users currently assigned to a specific role.", {
|
|
2453
|
+
project_id: z.string().describe("The project ID"),
|
|
2454
|
+
role_id: z.string().describe("The role ID")
|
|
2455
|
+
}, async ({ project_id, role_id }) => {
|
|
2456
|
+
const result = await apiClient.get(`/projects/${project_id}/roles/${role_id}/users`);
|
|
2457
|
+
return { content: [{
|
|
2458
|
+
type: "text",
|
|
2459
|
+
text: JSON.stringify(result, null, 2)
|
|
2460
|
+
}] };
|
|
2461
|
+
});
|
|
2462
|
+
server.tool("amba_messaging_list_conversations", "List message conversations for a project, newest first. Pagination via limit+offset.", {
|
|
2463
|
+
project_id: z.string().describe("The project ID"),
|
|
2464
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
2465
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
2466
|
+
}, async ({ project_id, limit, offset }) => {
|
|
2467
|
+
const query = {};
|
|
2468
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2469
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2470
|
+
const result = await apiClient.get(`/projects/${project_id}/messaging/conversations`, query);
|
|
2471
|
+
return { content: [{
|
|
2472
|
+
type: "text",
|
|
2473
|
+
text: JSON.stringify(result, null, 2)
|
|
2474
|
+
}] };
|
|
2475
|
+
});
|
|
2476
|
+
server.tool("amba_messaging_list_messages", "List messages within a single conversation, ordered by send time.", {
|
|
2477
|
+
project_id: z.string().describe("The project ID"),
|
|
2478
|
+
conversation_id: z.string().describe("The conversation ID"),
|
|
2479
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
2480
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
2481
|
+
}, async ({ project_id, conversation_id, limit, offset }) => {
|
|
2482
|
+
const query = {};
|
|
2483
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2484
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2485
|
+
const result = await apiClient.get(`/projects/${project_id}/messaging/conversations/${conversation_id}/messages`, query);
|
|
2486
|
+
return { content: [{
|
|
2487
|
+
type: "text",
|
|
2488
|
+
text: JSON.stringify(result, null, 2)
|
|
2489
|
+
}] };
|
|
2490
|
+
});
|
|
2491
|
+
server.tool("amba_messaging_delete_message", "Delete a single message permanently.", {
|
|
2492
|
+
project_id: z.string().describe("The project ID"),
|
|
2493
|
+
message_id: z.string().describe("The message ID")
|
|
2494
|
+
}, async ({ project_id, message_id }) => {
|
|
2495
|
+
const result = await apiClient.delete(`/projects/${project_id}/messaging/messages/${message_id}`);
|
|
2496
|
+
return { content: [{
|
|
2497
|
+
type: "text",
|
|
2498
|
+
text: JSON.stringify(result, null, 2)
|
|
2499
|
+
}] };
|
|
2500
|
+
});
|
|
2501
|
+
server.tool("amba_moderation_list_rules", "List moderation rule definitions. Rules drive automatic flag/escalate behavior on inbound content (feed posts, messages, reviews).", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
2502
|
+
const result = await apiClient.get(`/projects/${project_id}/moderation/rules`);
|
|
2503
|
+
return { content: [{
|
|
2504
|
+
type: "text",
|
|
2505
|
+
text: JSON.stringify(result, null, 2)
|
|
2506
|
+
}] };
|
|
2507
|
+
});
|
|
2508
|
+
server.tool("amba_moderation_update_rule", "Partially update a moderation rule. Only the supplied fields are touched.", {
|
|
2509
|
+
project_id: z.string().describe("The project ID"),
|
|
2510
|
+
rule_id: z.string().describe("The moderation rule ID"),
|
|
2511
|
+
name: z.string().optional().describe("Rule display name"),
|
|
2512
|
+
description: z.string().optional().describe("Rule description"),
|
|
2513
|
+
pattern: z.string().optional().describe("Match pattern (regex or keyword list)"),
|
|
2514
|
+
action: z.enum([
|
|
2515
|
+
"flag",
|
|
2516
|
+
"block",
|
|
2517
|
+
"escalate"
|
|
2518
|
+
]).optional().describe("Action to take when the rule fires"),
|
|
2519
|
+
is_active: z.boolean().optional().describe("Whether the rule is active")
|
|
2520
|
+
}, async ({ project_id, rule_id, name, description, pattern, action, is_active }) => {
|
|
2521
|
+
const payload = {};
|
|
2522
|
+
if (name !== void 0) payload.name = name;
|
|
2523
|
+
if (description !== void 0) payload.description = description;
|
|
2524
|
+
if (pattern !== void 0) payload.pattern = pattern;
|
|
2525
|
+
if (action !== void 0) payload.action = action;
|
|
2526
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
2527
|
+
const result = await apiClient.patch(`/projects/${project_id}/moderation/rules/${rule_id}`, payload);
|
|
2528
|
+
return { content: [{
|
|
2529
|
+
type: "text",
|
|
2530
|
+
text: JSON.stringify(result, null, 2)
|
|
2531
|
+
}] };
|
|
2532
|
+
});
|
|
2533
|
+
server.tool("amba_moderation_delete_rule", "Delete a moderation rule. New content will no longer be evaluated against this rule.", {
|
|
2534
|
+
project_id: z.string().describe("The project ID"),
|
|
2535
|
+
rule_id: z.string().describe("The moderation rule ID")
|
|
2536
|
+
}, async ({ project_id, rule_id }) => {
|
|
2537
|
+
const result = await apiClient.delete(`/projects/${project_id}/moderation/rules/${rule_id}`);
|
|
2538
|
+
return { content: [{
|
|
2539
|
+
type: "text",
|
|
2540
|
+
text: JSON.stringify(result, null, 2)
|
|
2541
|
+
}] };
|
|
2542
|
+
});
|
|
2543
|
+
server.tool("amba_moderation_list_trust", "List user trust scores. Useful to see who is currently trusted/untrusted by the moderation system.", {
|
|
2544
|
+
project_id: z.string().describe("The project ID"),
|
|
2545
|
+
min_score: z.number().optional().describe("Minimum trust score filter"),
|
|
2546
|
+
max_score: z.number().optional().describe("Maximum trust score filter"),
|
|
2547
|
+
limit: z.number().optional().describe("Page size"),
|
|
2548
|
+
offset: z.number().optional().describe("Offset for pagination")
|
|
2549
|
+
}, async ({ project_id, min_score, max_score, limit, offset }) => {
|
|
2550
|
+
const query = {};
|
|
2551
|
+
if (min_score !== void 0) query.min_score = String(min_score);
|
|
2552
|
+
if (max_score !== void 0) query.max_score = String(max_score);
|
|
2553
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2554
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2555
|
+
const result = await apiClient.get(`/projects/${project_id}/moderation/trust`, query);
|
|
2556
|
+
return { content: [{
|
|
2557
|
+
type: "text",
|
|
2558
|
+
text: JSON.stringify(result, null, 2)
|
|
2559
|
+
}] };
|
|
2560
|
+
});
|
|
2561
|
+
server.tool("amba_moderation_set_trust", "Set or upsert a user trust score. Higher scores reduce moderation friction; lower scores raise it.", {
|
|
2562
|
+
project_id: z.string().describe("The project ID"),
|
|
2563
|
+
user_id: z.string().describe("The app user ID"),
|
|
2564
|
+
score: z.number().describe("Trust score (typically 0-100)"),
|
|
2565
|
+
reason: z.string().optional().describe("Optional human-readable reason for the change")
|
|
2566
|
+
}, async ({ project_id, user_id, score, reason }) => {
|
|
2567
|
+
const payload = {
|
|
2568
|
+
user_id,
|
|
2569
|
+
score
|
|
2570
|
+
};
|
|
2571
|
+
if (reason !== void 0) payload.reason = reason;
|
|
2572
|
+
const result = await apiClient.post(`/projects/${project_id}/moderation/trust`, payload);
|
|
2573
|
+
return { content: [{
|
|
2574
|
+
type: "text",
|
|
2575
|
+
text: JSON.stringify(result, null, 2)
|
|
2576
|
+
}] };
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
//#endregion
|
|
2580
|
+
//#region src/tools/social.ts
|
|
2581
|
+
function registerTools$3(server, apiClient) {
|
|
2582
|
+
server.tool("amba_get_friendship_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 }) => {
|
|
2583
|
+
const result = await apiClient.get(`/projects/${project_id}/friends/stats`);
|
|
2584
|
+
return { content: [{
|
|
2585
|
+
type: "text",
|
|
2586
|
+
text: JSON.stringify(result, null, 2)
|
|
2587
|
+
}] };
|
|
2588
|
+
});
|
|
2589
|
+
server.tool("amba_create_group", "Create a group (guild) within a project. Groups let users form communities, with configurable visibility and max members.", {
|
|
2590
|
+
project_id: z.string().describe("The project ID"),
|
|
2591
|
+
name: z.string().describe("Group name"),
|
|
2592
|
+
owner_id: z.string().describe("The app_user ID who will own this group"),
|
|
2593
|
+
description: z.string().optional().describe("Group description"),
|
|
2594
|
+
avatar_url: z.string().optional().describe("URL for the group avatar"),
|
|
2595
|
+
is_public: z.boolean().optional().describe("Whether the group is publicly joinable (default true)"),
|
|
2596
|
+
max_members: z.number().optional().describe("Maximum number of members (default 100)"),
|
|
2597
|
+
metadata: z.record(z.unknown()).optional().describe("Arbitrary metadata key-value pairs")
|
|
2598
|
+
}, async ({ project_id, name, owner_id, description, avatar_url, is_public, max_members, metadata }) => {
|
|
2599
|
+
const payload = {
|
|
2600
|
+
name,
|
|
2601
|
+
owner_id
|
|
2602
|
+
};
|
|
2603
|
+
if (description !== void 0) payload.description = description;
|
|
2604
|
+
if (avatar_url !== void 0) payload.avatar_url = avatar_url;
|
|
2605
|
+
if (is_public !== void 0) payload.is_public = is_public;
|
|
2606
|
+
if (max_members !== void 0) payload.max_members = max_members;
|
|
2607
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
2608
|
+
const result = await apiClient.post(`/projects/${project_id}/groups`, payload);
|
|
2609
|
+
return { content: [{
|
|
2610
|
+
type: "text",
|
|
2611
|
+
text: JSON.stringify(result, null, 2)
|
|
2612
|
+
}] };
|
|
2613
|
+
});
|
|
2614
|
+
server.tool("amba_list_groups", "List all groups in a project. Returns groups with member counts, ordered by name.", {
|
|
2615
|
+
project_id: z.string().describe("The project ID"),
|
|
2616
|
+
limit: z.number().optional().describe("Max number of groups to return (default 50)"),
|
|
2617
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
2618
|
+
}, async ({ project_id, limit, offset }) => {
|
|
2619
|
+
const query = {};
|
|
2620
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2621
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2622
|
+
const result = await apiClient.get(`/projects/${project_id}/groups`, query);
|
|
2623
|
+
return { content: [{
|
|
2624
|
+
type: "text",
|
|
2625
|
+
text: JSON.stringify(result, null, 2)
|
|
2626
|
+
}] };
|
|
2627
|
+
});
|
|
2628
|
+
server.tool("amba_create_feed_rule", "Create a feed generation rule. When the specified engagement event occurs, a feed item is automatically created with the given action.", {
|
|
2629
|
+
project_id: z.string().describe("The project ID"),
|
|
2630
|
+
source_event: z.string().describe("The engagement event name that triggers feed item creation (e.g. \"streak_milestone\", \"achievement_unlocked\")"),
|
|
2631
|
+
action: z.string().describe("The action string to use in the feed item (e.g. \"reached_streak_milestone\")"),
|
|
2632
|
+
target_type: z.string().optional().describe("Optional target type for the feed item")
|
|
2633
|
+
}, async ({ project_id, source_event, action, target_type }) => {
|
|
2634
|
+
const payload = {
|
|
2635
|
+
source_event,
|
|
2636
|
+
action
|
|
2637
|
+
};
|
|
2638
|
+
if (target_type !== void 0) payload.target_type = target_type;
|
|
2639
|
+
const result = await apiClient.post(`/projects/${project_id}/feeds/rules`, payload);
|
|
2640
|
+
return { content: [{
|
|
2641
|
+
type: "text",
|
|
2642
|
+
text: JSON.stringify(result, null, 2)
|
|
2643
|
+
}] };
|
|
2644
|
+
});
|
|
2645
|
+
server.tool("amba_feeds_list_rules", "List all feed generation rules for a project, ordered by source_event.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
2646
|
+
const result = await apiClient.get(`/projects/${project_id}/feeds/rules`);
|
|
2647
|
+
return { content: [{
|
|
2648
|
+
type: "text",
|
|
2649
|
+
text: JSON.stringify(result, null, 2)
|
|
2650
|
+
}] };
|
|
2651
|
+
});
|
|
2652
|
+
server.tool("amba_feeds_patch_rule", "Update a feed rule. Omitted fields are left unchanged.", {
|
|
2653
|
+
project_id: z.string().describe("The project ID"),
|
|
2654
|
+
rule_id: z.string().describe("The feed rule ID"),
|
|
2655
|
+
source_event: z.string().optional().describe("New source engagement event name"),
|
|
2656
|
+
action: z.string().optional().describe("New action string for generated feed items"),
|
|
2657
|
+
target_type: z.string().optional().describe("New target type"),
|
|
2658
|
+
is_active: z.boolean().optional().describe("Toggle whether the rule is active")
|
|
2659
|
+
}, async ({ project_id, rule_id, source_event, action, target_type, is_active }) => {
|
|
2660
|
+
const payload = {};
|
|
2661
|
+
if (source_event !== void 0) payload.source_event = source_event;
|
|
2662
|
+
if (action !== void 0) payload.action = action;
|
|
2663
|
+
if (target_type !== void 0) payload.target_type = target_type;
|
|
2664
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
2665
|
+
const result = await apiClient.patch(`/projects/${project_id}/feeds/rules/${rule_id}`, payload);
|
|
2666
|
+
return { content: [{
|
|
2667
|
+
type: "text",
|
|
2668
|
+
text: JSON.stringify(result, null, 2)
|
|
2669
|
+
}] };
|
|
2670
|
+
});
|
|
2671
|
+
server.tool("amba_feeds_delete_rule", "Delete a feed rule. Existing feed items generated by the rule are preserved.", {
|
|
2672
|
+
project_id: z.string().describe("The project ID"),
|
|
2673
|
+
rule_id: z.string().describe("The feed rule ID")
|
|
2674
|
+
}, async ({ project_id, rule_id }) => {
|
|
2675
|
+
const result = await apiClient.delete(`/projects/${project_id}/feeds/rules/${rule_id}`);
|
|
2676
|
+
return { content: [{
|
|
2677
|
+
type: "text",
|
|
2678
|
+
text: JSON.stringify(result, null, 2)
|
|
2679
|
+
}] };
|
|
2680
|
+
});
|
|
2681
|
+
server.tool("amba_get_messaging_stats", "Get in-app messaging statistics for a project. Returns total conversations, total messages, and active conversations in the last 24 hours.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
2682
|
+
const result = await apiClient.get(`/projects/${project_id}/messaging/stats`);
|
|
2683
|
+
return { content: [{
|
|
2684
|
+
type: "text",
|
|
2685
|
+
text: JSON.stringify(result, null, 2)
|
|
2686
|
+
}] };
|
|
2687
|
+
});
|
|
2688
|
+
server.tool("amba_get_review_stats", "Get review and rating statistics for a project. Returns total reviews, average rating, pending approvals, and rating distribution.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
2689
|
+
const result = await apiClient.get(`/projects/${project_id}/reviews/stats`);
|
|
2690
|
+
return { content: [{
|
|
2691
|
+
type: "text",
|
|
2692
|
+
text: JSON.stringify(result, null, 2)
|
|
2693
|
+
}] };
|
|
2694
|
+
});
|
|
2695
|
+
server.tool("amba_reviews_list", "List reviews for a project, newest first. Supports pagination and filtering by approval status.", {
|
|
2696
|
+
project_id: z.string().describe("The project ID"),
|
|
2697
|
+
is_approved: z.boolean().optional().describe("Filter to approved (true) or pending (false) reviews. Omit for all."),
|
|
2698
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
2699
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
2700
|
+
}, async ({ project_id, is_approved, limit, offset }) => {
|
|
2701
|
+
const query = {};
|
|
2702
|
+
if (is_approved !== void 0) query.is_approved = String(is_approved);
|
|
2703
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2704
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2705
|
+
const result = await apiClient.get(`/projects/${project_id}/reviews`, query);
|
|
2706
|
+
return { content: [{
|
|
2707
|
+
type: "text",
|
|
2708
|
+
text: JSON.stringify(result, null, 2)
|
|
2709
|
+
}] };
|
|
2710
|
+
});
|
|
2711
|
+
server.tool("amba_reviews_patch", "Approve or unapprove a review by toggling `is_approved`. Unapproved reviews are hidden from public listings.", {
|
|
2712
|
+
project_id: z.string().describe("The project ID"),
|
|
2713
|
+
review_id: z.string().describe("The review ID"),
|
|
2714
|
+
is_approved: z.boolean().describe("True to approve, false to hide")
|
|
2715
|
+
}, async ({ project_id, review_id, is_approved }) => {
|
|
2716
|
+
const result = await apiClient.patch(`/projects/${project_id}/reviews/${review_id}`, { is_approved });
|
|
2717
|
+
return { content: [{
|
|
2718
|
+
type: "text",
|
|
2719
|
+
text: JSON.stringify(result, null, 2)
|
|
2720
|
+
}] };
|
|
2721
|
+
});
|
|
2722
|
+
server.tool("amba_reviews_delete", "Delete a review permanently.", {
|
|
2723
|
+
project_id: z.string().describe("The project ID"),
|
|
2724
|
+
review_id: z.string().describe("The review ID")
|
|
2725
|
+
}, async ({ project_id, review_id }) => {
|
|
2726
|
+
const result = await apiClient.delete(`/projects/${project_id}/reviews/${review_id}`);
|
|
2727
|
+
return { content: [{
|
|
2728
|
+
type: "text",
|
|
2729
|
+
text: JSON.stringify(result, null, 2)
|
|
2730
|
+
}] };
|
|
2731
|
+
});
|
|
2732
|
+
server.tool("amba_reviews_list_items", "List reviewable items for a project, ordered by average rating. Use this to see aggregate ratings per reviewable entity.", {
|
|
2733
|
+
project_id: z.string().describe("The project ID"),
|
|
2734
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
2735
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
2736
|
+
}, async ({ project_id, limit, offset }) => {
|
|
2737
|
+
const query = {};
|
|
2738
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2739
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2740
|
+
const result = await apiClient.get(`/projects/${project_id}/reviews/items`, query);
|
|
2741
|
+
return { content: [{
|
|
2742
|
+
type: "text",
|
|
2743
|
+
text: JSON.stringify(result, null, 2)
|
|
2744
|
+
}] };
|
|
2745
|
+
});
|
|
2746
|
+
server.tool("amba_reviews_export", "Export reviews for a project as CSV (default) or NDJSON. Streams via DB cursor on the API side. Filters: `since` (created_at lower bound), `rating_min`/`rating_max` (1-5). The MCP tool reads back at most ~1 MiB; for very large tenants prefer `format: \"ndjson\"` and narrow with rating + since filters. The returned `truncated` flag indicates whether the body was clipped.", {
|
|
2747
|
+
project_id: z.string().describe("The project ID"),
|
|
2748
|
+
format: z.enum(["csv", "ndjson"]).optional().describe("Response format. Default csv. Recommend ndjson for parsability."),
|
|
2749
|
+
since: z.string().optional().describe("ISO-8601 lower bound on `created_at`."),
|
|
2750
|
+
rating_min: z.number().int().min(1).max(5).optional().describe("Minimum star rating (1-5)."),
|
|
2751
|
+
rating_max: z.number().int().min(1).max(5).optional().describe("Maximum star rating (1-5).")
|
|
2752
|
+
}, async ({ project_id, format, since, rating_min, rating_max }) => {
|
|
2753
|
+
const query = {};
|
|
2754
|
+
if (format !== void 0) query.format = format;
|
|
2755
|
+
if (since !== void 0) query.since = since;
|
|
2756
|
+
if (rating_min !== void 0) query.rating_min = String(rating_min);
|
|
2757
|
+
if (rating_max !== void 0) query.rating_max = String(rating_max);
|
|
2758
|
+
const { body, truncated, contentType } = await apiClient.getRaw(`/projects/${project_id}/reviews/export`, query);
|
|
2759
|
+
return { content: [{
|
|
2760
|
+
type: "text",
|
|
2761
|
+
text: `# format=${format ?? "csv"} truncated=${truncated} content-type=${contentType ?? "unknown"}\n` + body
|
|
2762
|
+
}] };
|
|
2763
|
+
});
|
|
2764
|
+
server.tool("amba_list_friendships", "List friendship rows for a project. Filter by status, user, or recency. Useful for moderation and engagement analytics.", {
|
|
2765
|
+
project_id: z.string().describe("The project ID"),
|
|
2766
|
+
status: z.enum([
|
|
2767
|
+
"pending",
|
|
2768
|
+
"accepted",
|
|
2769
|
+
"blocked"
|
|
2770
|
+
]).optional().describe("Filter by friendship status"),
|
|
2771
|
+
user_id: z.string().optional().describe("Filter to friendships involving this app user"),
|
|
2772
|
+
limit: z.number().optional().describe("Page size (default 50)"),
|
|
2773
|
+
offset: z.number().optional().describe("Offset for pagination")
|
|
2774
|
+
}, async ({ project_id, status, user_id, limit, offset }) => {
|
|
2775
|
+
const query = {};
|
|
2776
|
+
if (status !== void 0) query.status = status;
|
|
2777
|
+
if (user_id !== void 0) query.user_id = user_id;
|
|
2778
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2779
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2780
|
+
const result = await apiClient.get(`/projects/${project_id}/friends`, query);
|
|
2781
|
+
return { content: [{
|
|
2782
|
+
type: "text",
|
|
2783
|
+
text: JSON.stringify(result, null, 2)
|
|
2784
|
+
}] };
|
|
2785
|
+
});
|
|
2786
|
+
server.tool("amba_delete_friendship", "Delete a friendship row. Used for moderation or cleanup. Idempotent.", {
|
|
2787
|
+
project_id: z.string().describe("The project ID"),
|
|
2788
|
+
friendship_id: z.string().describe("The friendship ID")
|
|
2789
|
+
}, async ({ project_id, friendship_id }) => {
|
|
2790
|
+
const result = await apiClient.delete(`/projects/${project_id}/friends/${friendship_id}`);
|
|
2791
|
+
return { content: [{
|
|
2792
|
+
type: "text",
|
|
2793
|
+
text: JSON.stringify(result, null, 2)
|
|
2794
|
+
}] };
|
|
2795
|
+
});
|
|
2796
|
+
server.tool("amba_update_group", "Partially update a group definition. Only the supplied fields are touched.", {
|
|
2797
|
+
project_id: z.string().describe("The project ID"),
|
|
2798
|
+
group_id: z.string().describe("The group ID"),
|
|
2799
|
+
name: z.string().optional().describe("Group name"),
|
|
2800
|
+
description: z.string().optional().describe("Group description"),
|
|
2801
|
+
avatar_url: z.string().optional().describe("Group avatar URL"),
|
|
2802
|
+
is_public: z.boolean().optional().describe("Whether the group is publicly joinable"),
|
|
2803
|
+
max_members: z.number().optional().describe("Maximum members allowed"),
|
|
2804
|
+
metadata: z.record(z.unknown()).optional().describe("Arbitrary metadata")
|
|
2805
|
+
}, async ({ project_id, group_id, name, description, avatar_url, is_public, max_members, metadata }) => {
|
|
2806
|
+
const payload = {};
|
|
2807
|
+
if (name !== void 0) payload.name = name;
|
|
2808
|
+
if (description !== void 0) payload.description = description;
|
|
2809
|
+
if (avatar_url !== void 0) payload.avatar_url = avatar_url;
|
|
2810
|
+
if (is_public !== void 0) payload.is_public = is_public;
|
|
2811
|
+
if (max_members !== void 0) payload.max_members = max_members;
|
|
2812
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
2813
|
+
const result = await apiClient.patch(`/projects/${project_id}/groups/${group_id}`, payload);
|
|
2814
|
+
return { content: [{
|
|
2815
|
+
type: "text",
|
|
2816
|
+
text: JSON.stringify(result, null, 2)
|
|
2817
|
+
}] };
|
|
2818
|
+
});
|
|
2819
|
+
server.tool("amba_delete_group", "Delete a group. All membership rows for the group are also removed.", {
|
|
2820
|
+
project_id: z.string().describe("The project ID"),
|
|
2821
|
+
group_id: z.string().describe("The group ID")
|
|
2822
|
+
}, async ({ project_id, group_id }) => {
|
|
2823
|
+
const result = await apiClient.delete(`/projects/${project_id}/groups/${group_id}`);
|
|
2824
|
+
return { content: [{
|
|
2825
|
+
type: "text",
|
|
2826
|
+
text: JSON.stringify(result, null, 2)
|
|
2827
|
+
}] };
|
|
2828
|
+
});
|
|
2829
|
+
server.tool("amba_list_group_members", "List members of a single group, with their join timestamps and roles.", {
|
|
2830
|
+
project_id: z.string().describe("The project ID"),
|
|
2831
|
+
group_id: z.string().describe("The group ID"),
|
|
2832
|
+
limit: z.number().optional().describe("Page size"),
|
|
2833
|
+
offset: z.number().optional().describe("Offset for pagination")
|
|
2834
|
+
}, async ({ project_id, group_id, limit, offset }) => {
|
|
2835
|
+
const query = {};
|
|
2836
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2837
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2838
|
+
const result = await apiClient.get(`/projects/${project_id}/groups/${group_id}/members`, query);
|
|
2839
|
+
return { content: [{
|
|
2840
|
+
type: "text",
|
|
2841
|
+
text: JSON.stringify(result, null, 2)
|
|
2842
|
+
}] };
|
|
2843
|
+
});
|
|
2844
|
+
server.tool("amba_update_group_member", "Update a group member's role or metadata. Use this to promote/demote members or amend their custom data.", {
|
|
2845
|
+
project_id: z.string().describe("The project ID"),
|
|
2846
|
+
group_id: z.string().describe("The group ID"),
|
|
2847
|
+
member_id: z.string().describe("The membership row ID"),
|
|
2848
|
+
role: z.enum([
|
|
2849
|
+
"member",
|
|
2850
|
+
"moderator",
|
|
2851
|
+
"owner"
|
|
2852
|
+
]).optional().describe("Member role within the group"),
|
|
2853
|
+
metadata: z.record(z.unknown()).optional().describe("Custom metadata")
|
|
2854
|
+
}, async ({ project_id, group_id, member_id, role, metadata }) => {
|
|
2855
|
+
const payload = {};
|
|
2856
|
+
if (role !== void 0) payload.role = role;
|
|
2857
|
+
if (metadata !== void 0) payload.metadata = metadata;
|
|
2858
|
+
const result = await apiClient.patch(`/projects/${project_id}/groups/${group_id}/members/${member_id}`, payload);
|
|
2859
|
+
return { content: [{
|
|
2860
|
+
type: "text",
|
|
2861
|
+
text: JSON.stringify(result, null, 2)
|
|
2862
|
+
}] };
|
|
2863
|
+
});
|
|
2864
|
+
server.tool("amba_remove_group_member", "Remove a member from a group (kick or self-leave).", {
|
|
2865
|
+
project_id: z.string().describe("The project ID"),
|
|
2866
|
+
group_id: z.string().describe("The group ID"),
|
|
2867
|
+
member_id: z.string().describe("The membership row ID")
|
|
2868
|
+
}, async ({ project_id, group_id, member_id }) => {
|
|
2869
|
+
const result = await apiClient.delete(`/projects/${project_id}/groups/${group_id}/members/${member_id}`);
|
|
2870
|
+
return { content: [{
|
|
2871
|
+
type: "text",
|
|
2872
|
+
text: JSON.stringify(result, null, 2)
|
|
2873
|
+
}] };
|
|
2874
|
+
});
|
|
2875
|
+
server.tool("amba_feeds_list_items", "List feed items in a project. Useful for moderation review or analytics.", {
|
|
2876
|
+
project_id: z.string().describe("The project ID"),
|
|
2877
|
+
user_id: z.string().optional().describe("Filter to feed items posted by this user"),
|
|
2878
|
+
since: z.string().optional().describe("ISO 8601 lower-bound timestamp"),
|
|
2879
|
+
limit: z.number().optional().describe("Page size"),
|
|
2880
|
+
offset: z.number().optional().describe("Offset for pagination")
|
|
2881
|
+
}, async ({ project_id, user_id, since, limit, offset }) => {
|
|
2882
|
+
const query = {};
|
|
2883
|
+
if (user_id !== void 0) query.user_id = user_id;
|
|
2884
|
+
if (since !== void 0) query.since = since;
|
|
2885
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2886
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2887
|
+
const result = await apiClient.get(`/projects/${project_id}/feeds/items`, query);
|
|
2888
|
+
return { content: [{
|
|
2889
|
+
type: "text",
|
|
2890
|
+
text: JSON.stringify(result, null, 2)
|
|
2891
|
+
}] };
|
|
2892
|
+
});
|
|
2893
|
+
server.tool("amba_feeds_delete_item", "Delete a feed item. Used for moderation. Hard delete — the row is removed.", {
|
|
2894
|
+
project_id: z.string().describe("The project ID"),
|
|
2895
|
+
item_id: z.string().describe("The feed item ID")
|
|
2896
|
+
}, async ({ project_id, item_id }) => {
|
|
2897
|
+
const result = await apiClient.delete(`/projects/${project_id}/feeds/items/${item_id}`);
|
|
2898
|
+
return { content: [{
|
|
2899
|
+
type: "text",
|
|
2900
|
+
text: JSON.stringify(result, null, 2)
|
|
2901
|
+
}] };
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
//#endregion
|
|
2905
|
+
//#region src/tools/xp.ts
|
|
2906
|
+
function registerTools$2(server, apiClient) {
|
|
2907
|
+
server.tool("amba_create_xp_rule", "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.", {
|
|
2908
|
+
project_id: z.string().describe("The project ID"),
|
|
2909
|
+
name: z.string().describe("Rule name (e.g. \"Workout Completed\", \"Daily Login Bonus\")"),
|
|
2910
|
+
event_name: z.string().describe("The engagement event name that triggers XP (e.g. \"workout_completed\", \"app_open\")"),
|
|
2911
|
+
xp_amount: z.number().describe("Amount of XP to award per event"),
|
|
2912
|
+
max_per_day: z.number().optional().describe("Maximum number of times this rule can fire per user per day (null = unlimited)"),
|
|
2913
|
+
cooldown_seconds: z.number().optional().describe("Minimum seconds between XP awards for the same user (default 0)")
|
|
2914
|
+
}, async ({ project_id, name, event_name, xp_amount, max_per_day, cooldown_seconds }) => {
|
|
2915
|
+
const payload = {
|
|
2916
|
+
name,
|
|
2917
|
+
event_name,
|
|
2918
|
+
xp_amount
|
|
2919
|
+
};
|
|
2920
|
+
if (max_per_day !== void 0) payload.max_per_day = max_per_day;
|
|
2921
|
+
if (cooldown_seconds !== void 0) payload.cooldown_seconds = cooldown_seconds;
|
|
2922
|
+
const result = await apiClient.post(`/projects/${project_id}/xp`, payload);
|
|
2923
|
+
return { content: [{
|
|
2924
|
+
type: "text",
|
|
2925
|
+
text: JSON.stringify(result, null, 2)
|
|
2926
|
+
}] };
|
|
2927
|
+
});
|
|
2928
|
+
server.tool("amba_list_xp_rules", "List all XP rules configured for a project. Shows event triggers, XP amounts, daily caps, and cooldowns.", { project_id: z.string().describe("The project ID") }, async ({ project_id }) => {
|
|
2929
|
+
const result = await apiClient.get(`/projects/${project_id}/xp`);
|
|
2930
|
+
return { content: [{
|
|
2931
|
+
type: "text",
|
|
2932
|
+
text: JSON.stringify(result, null, 2)
|
|
2933
|
+
}] };
|
|
2934
|
+
});
|
|
2935
|
+
server.tool("amba_get_user_xp", "Get a specific user's XP total, current level, and XP history. Useful for debugging or customer support.", {
|
|
2936
|
+
project_id: z.string().describe("The project ID"),
|
|
2937
|
+
user_id: z.string().describe("The app user ID")
|
|
2938
|
+
}, async ({ project_id, user_id }) => {
|
|
2939
|
+
const result = await apiClient.get(`/projects/${project_id}/xp/users/${user_id}`);
|
|
2940
|
+
return { content: [{
|
|
2941
|
+
type: "text",
|
|
2942
|
+
text: JSON.stringify(result, null, 2)
|
|
2943
|
+
}] };
|
|
2944
|
+
});
|
|
2945
|
+
server.tool("amba_xp_update_rule", "Update an XP rule. Only the supplied fields are changed; omitted fields are left untouched.", {
|
|
2946
|
+
project_id: z.string().describe("The project ID"),
|
|
2947
|
+
rule_id: z.string().describe("The XP rule ID"),
|
|
2948
|
+
name: z.string().optional().describe("New rule name"),
|
|
2949
|
+
event_name: z.string().optional().describe("New trigger event name"),
|
|
2950
|
+
xp_amount: z.number().optional().describe("New XP amount per event"),
|
|
2951
|
+
max_per_day: z.number().nullable().optional().describe("New per-user-per-day cap (null = unlimited)"),
|
|
2952
|
+
cooldown_seconds: z.number().optional().describe("New cooldown between awards in seconds"),
|
|
2953
|
+
is_active: z.boolean().optional().describe("Toggle whether the rule is active")
|
|
2954
|
+
}, async ({ project_id, rule_id, name, event_name, xp_amount, max_per_day, cooldown_seconds, is_active }) => {
|
|
2955
|
+
const payload = {};
|
|
2956
|
+
if (name !== void 0) payload.name = name;
|
|
2957
|
+
if (event_name !== void 0) payload.event_name = event_name;
|
|
2958
|
+
if (xp_amount !== void 0) payload.xp_amount = xp_amount;
|
|
2959
|
+
if (max_per_day !== void 0) payload.max_per_day = max_per_day;
|
|
2960
|
+
if (cooldown_seconds !== void 0) payload.cooldown_seconds = cooldown_seconds;
|
|
2961
|
+
if (is_active !== void 0) payload.is_active = is_active;
|
|
2962
|
+
const result = await apiClient.patch(`/projects/${project_id}/xp/${rule_id}`, payload);
|
|
2963
|
+
return { content: [{
|
|
2964
|
+
type: "text",
|
|
2965
|
+
text: JSON.stringify(result, null, 2)
|
|
2966
|
+
}] };
|
|
2967
|
+
});
|
|
2968
|
+
server.tool("amba_xp_delete_rule", "Delete an XP rule. Historical user_xp rows are preserved.", {
|
|
2969
|
+
project_id: z.string().describe("The project ID"),
|
|
2970
|
+
rule_id: z.string().describe("The XP rule ID")
|
|
2971
|
+
}, async ({ project_id, rule_id }) => {
|
|
2972
|
+
const result = await apiClient.delete(`/projects/${project_id}/xp/${rule_id}`);
|
|
2973
|
+
return { content: [{
|
|
2974
|
+
type: "text",
|
|
2975
|
+
text: JSON.stringify(result, null, 2)
|
|
2976
|
+
}] };
|
|
2977
|
+
});
|
|
2978
|
+
server.tool("amba_xp_list_users", "List users by total XP (leaderboard view). Ordered by total_xp descending with pagination.", {
|
|
2979
|
+
project_id: z.string().describe("The project ID"),
|
|
2980
|
+
limit: z.number().optional().describe("Max results to return (default 50)"),
|
|
2981
|
+
offset: z.number().optional().describe("Offset for pagination (default 0)")
|
|
2982
|
+
}, async ({ project_id, limit, offset }) => {
|
|
2983
|
+
const query = {};
|
|
2984
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
2985
|
+
if (offset !== void 0) query.offset = String(offset);
|
|
2986
|
+
const result = await apiClient.get(`/projects/${project_id}/xp/users`, query);
|
|
2987
|
+
return { content: [{
|
|
2988
|
+
type: "text",
|
|
2989
|
+
text: JSON.stringify(result, null, 2)
|
|
2990
|
+
}] };
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
//#endregion
|
|
2994
|
+
//#region src/tools/events.ts
|
|
2995
|
+
function registerTools$1(server, apiClient) {
|
|
2996
|
+
server.tool("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.", {
|
|
2997
|
+
project_id: z.string().describe("The project ID"),
|
|
2998
|
+
since: z.string().optional().describe("ISO-8601 lower bound for occurred_at. Defaults to 24h ago."),
|
|
2999
|
+
until: z.string().optional().describe("ISO-8601 upper bound for occurred_at."),
|
|
3000
|
+
event_name: z.string().optional().describe("Filter to a specific event_name."),
|
|
3001
|
+
user_id: z.string().optional().describe("Filter to a single app_user UUID."),
|
|
3002
|
+
limit: z.number().optional().describe("Page size (default 100, max 1000)."),
|
|
3003
|
+
cursor: z.string().optional().describe("Opaque cursor returned by a previous page as `next_cursor`.")
|
|
3004
|
+
}, async ({ project_id, since, until, event_name, user_id, limit, cursor }) => {
|
|
3005
|
+
const query = {};
|
|
3006
|
+
if (since !== void 0) query.since = since;
|
|
3007
|
+
if (until !== void 0) query.until = until;
|
|
3008
|
+
if (event_name !== void 0) query.event_name = event_name;
|
|
3009
|
+
if (user_id !== void 0) query.user_id = user_id;
|
|
3010
|
+
if (limit !== void 0) query.limit = String(limit);
|
|
3011
|
+
if (cursor !== void 0) query.cursor = cursor;
|
|
3012
|
+
const result = await apiClient.get(`/projects/${project_id}/events`, query);
|
|
3013
|
+
return { content: [{
|
|
3014
|
+
type: "text",
|
|
3015
|
+
text: JSON.stringify(result, null, 2)
|
|
3016
|
+
}] };
|
|
3017
|
+
});
|
|
3018
|
+
server.tool("amba_events_count", "Aggregate event counts for a project across a time window. `since` is required; range is capped at 90 days. Optional `group_by` returns per-day or per-event_name buckets in addition to the total.", {
|
|
3019
|
+
project_id: z.string().describe("The project ID"),
|
|
3020
|
+
since: z.string().describe("ISO-8601 lower bound for occurred_at (required)."),
|
|
3021
|
+
until: z.string().optional().describe("ISO-8601 upper bound for occurred_at. Defaults to now."),
|
|
3022
|
+
event_name: z.string().optional().describe("Filter to a specific event_name."),
|
|
3023
|
+
group_by: z.enum(["day", "event_name"]).optional().describe("Bucket results by calendar day (UTC) or event_name. Omit for total only.")
|
|
3024
|
+
}, async ({ project_id, since, until, event_name, group_by }) => {
|
|
3025
|
+
const query = { since };
|
|
3026
|
+
if (until !== void 0) query.until = until;
|
|
3027
|
+
if (event_name !== void 0) query.event_name = event_name;
|
|
3028
|
+
if (group_by !== void 0) query.group_by = group_by;
|
|
3029
|
+
const result = await apiClient.get(`/projects/${project_id}/events/count`, query);
|
|
3030
|
+
return { content: [{
|
|
3031
|
+
type: "text",
|
|
3032
|
+
text: JSON.stringify(result, null, 2)
|
|
3033
|
+
}] };
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
3036
|
+
//#endregion
|
|
3037
|
+
//#region src/tools/auth.ts
|
|
3038
|
+
async function authFetch(apiClient, options) {
|
|
3039
|
+
const url = `${apiClient.getApiRoot()}${options.path}`;
|
|
3040
|
+
const headers = {
|
|
3041
|
+
"Content-Type": "application/json",
|
|
3042
|
+
Accept: "application/json"
|
|
3043
|
+
};
|
|
3044
|
+
if (options.bearer) headers["Authorization"] = `Bearer ${options.bearer}`;
|
|
3045
|
+
const res = await fetch(url, {
|
|
3046
|
+
method: options.method,
|
|
3047
|
+
headers,
|
|
3048
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
3049
|
+
});
|
|
3050
|
+
let body;
|
|
3051
|
+
try {
|
|
3052
|
+
body = await res.json();
|
|
3053
|
+
} catch {
|
|
3054
|
+
body = { error: {
|
|
3055
|
+
code: "INVALID_RESPONSE",
|
|
3056
|
+
message: res.statusText
|
|
3057
|
+
} };
|
|
3058
|
+
}
|
|
3059
|
+
return {
|
|
3060
|
+
status: res.status,
|
|
3061
|
+
body
|
|
3062
|
+
};
|
|
3063
|
+
}
|
|
3064
|
+
function jsonResult(payload) {
|
|
3065
|
+
return { content: [{
|
|
3066
|
+
type: "text",
|
|
3067
|
+
text: JSON.stringify(payload, null, 2)
|
|
3068
|
+
}] };
|
|
3069
|
+
}
|
|
3070
|
+
/**
|
|
3071
|
+
* Flatten an authFetch result into the agent-facing tool payload.
|
|
3072
|
+
*
|
|
3073
|
+
* The body is spread FIRST so the HTTP `status` written last cannot be
|
|
3074
|
+
* shadowed by a colliding top-level `status` field in the API response.
|
|
3075
|
+
* Today no /auth/developer handler emits a top-level `status` key, but
|
|
3076
|
+
* agents read `parsed.status` to distinguish 2xx from 4xx/5xx — making
|
|
3077
|
+
* that key authoritative defends against a future API change silently
|
|
3078
|
+
* breaking error handling.
|
|
3079
|
+
*/
|
|
3080
|
+
function passthroughResult(result) {
|
|
3081
|
+
return jsonResult({
|
|
3082
|
+
...result.body ?? {},
|
|
3083
|
+
status: result.status
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
function registerTools(server, apiClient) {
|
|
3087
|
+
server.tool("amba_developer_signup", [
|
|
3088
|
+
"Create a new Amba developer account. Returns a long-lived Personal Access Token (PAT)",
|
|
3089
|
+
"plus a real isolated Neon-backed project (provisioning asynchronously).",
|
|
3090
|
+
"Use the PAT for agent flows: pass the returned `pat` as the inbound Bearer for every",
|
|
3091
|
+
"subsequent MCP call — no refresh dance required.",
|
|
3092
|
+
"Verify your account via the returned `project.verify_url`. Poll",
|
|
3093
|
+
"`amba_get_provisioning_status` until the project flips to status='active' (~10s)",
|
|
3094
|
+
"before issuing client-plane traffic against the returned API keys.",
|
|
3095
|
+
"Email is normalized to lowercase. Password must be at least 8 characters.",
|
|
3096
|
+
"Errors: 409 EMAIL_EXISTS, 400 WEAK_PASSWORD/INVALID_INPUT, 429 if rate-limited (5/min, 50/day per IP)."
|
|
3097
|
+
].join(" "), {
|
|
3098
|
+
email: z.string().email().describe("Developer email address. Will be lowercased."),
|
|
3099
|
+
password: z.string().min(8).describe("Password, minimum 8 characters."),
|
|
3100
|
+
name: z.string().optional().describe("Optional display name.")
|
|
3101
|
+
}, async ({ email, password, name }) => {
|
|
3102
|
+
const body = {
|
|
3103
|
+
email,
|
|
3104
|
+
password
|
|
3105
|
+
};
|
|
3106
|
+
if (name !== void 0) body.name = name;
|
|
3107
|
+
return passthroughResult(await authFetch(apiClient, {
|
|
3108
|
+
method: "POST",
|
|
3109
|
+
path: "/auth/developer/signup",
|
|
3110
|
+
body
|
|
3111
|
+
}));
|
|
3112
|
+
});
|
|
3113
|
+
server.tool("amba_developer_login", [
|
|
3114
|
+
"Log in to an existing Amba developer account. Returns access_token + refresh_token + developer record.",
|
|
3115
|
+
"Use the returned access_token as the inbound Bearer header on every subsequent MCP call.",
|
|
3116
|
+
"Errors: 401 INVALID_CREDENTIALS, 429 if rate-limited (10/min, 100/day per IP)."
|
|
3117
|
+
].join(" "), {
|
|
3118
|
+
email: z.string().email().describe("Developer email address."),
|
|
3119
|
+
password: z.string().describe("Developer password.")
|
|
3120
|
+
}, async ({ email, password }) => {
|
|
3121
|
+
return passthroughResult(await authFetch(apiClient, {
|
|
3122
|
+
method: "POST",
|
|
3123
|
+
path: "/auth/developer/login",
|
|
3124
|
+
body: {
|
|
3125
|
+
email,
|
|
3126
|
+
password
|
|
3127
|
+
}
|
|
3128
|
+
}));
|
|
3129
|
+
});
|
|
3130
|
+
server.tool("amba_developer_refresh", [
|
|
3131
|
+
"Rotate a developer access_token using the refresh_token previously issued by signup or login.",
|
|
3132
|
+
"The old refresh_token is revoked and a new pair is issued — do NOT keep using the old one.",
|
|
3133
|
+
"Errors: 401 INVALID_TOKEN if refresh_token is expired/revoked/unknown, 429 if rate-limited."
|
|
3134
|
+
].join(" "), { refresh_token: z.string().describe("The refresh_token from a prior signup/login response.") }, async ({ refresh_token }) => {
|
|
3135
|
+
return passthroughResult(await authFetch(apiClient, {
|
|
3136
|
+
method: "POST",
|
|
3137
|
+
path: "/auth/developer/refresh",
|
|
3138
|
+
body: { refresh_token }
|
|
3139
|
+
}));
|
|
3140
|
+
});
|
|
3141
|
+
server.tool("amba_developer_rotate_pat", [
|
|
3142
|
+
"Rotate the developer PAT. The old PAT stops working immediately (may take up to 30s to propagate).",
|
|
3143
|
+
"Use this if you suspect the current PAT has been exposed, or as a routine credential-hygiene step.",
|
|
3144
|
+
"Requires a valid Bearer (PAT or JWT) on the inbound request — does NOT bypass auth.",
|
|
3145
|
+
"Returns the new pat + pat_prefix + pat_last_4. The full pat is returned ONCE — store it immediately.",
|
|
3146
|
+
"Errors: 401 if Bearer is missing/invalid, 500 ROTATE_FAILED on DB error."
|
|
3147
|
+
].join(" "), {}, async () => {
|
|
3148
|
+
const bearer = await apiClient.resolveTokenOrNull();
|
|
3149
|
+
if (!bearer) return jsonResult({
|
|
3150
|
+
status: 401,
|
|
3151
|
+
error: {
|
|
3152
|
+
code: "MISSING_BEARER",
|
|
3153
|
+
message: "No developer access token available. Call amba_developer_signup or amba_developer_login first."
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
3156
|
+
return passthroughResult(await authFetch(apiClient, {
|
|
3157
|
+
method: "POST",
|
|
3158
|
+
path: "/auth/developer/pat/rotate",
|
|
3159
|
+
bearer
|
|
3160
|
+
}));
|
|
3161
|
+
});
|
|
3162
|
+
server.tool("amba_developer_me", [
|
|
3163
|
+
"Return the developer profile (id, email, name, oauth_providers, avatar_url, github_username)",
|
|
3164
|
+
"for the access_token currently sent as the inbound Bearer. Useful for confirming the agent",
|
|
3165
|
+
"is authenticated as the expected developer before taking destructive actions.",
|
|
3166
|
+
"Errors: 401 if Bearer is missing/invalid, 404 NOT_FOUND if developer was deleted."
|
|
3167
|
+
].join(" "), {}, async () => {
|
|
3168
|
+
const bearer = await apiClient.resolveTokenOrNull();
|
|
3169
|
+
if (!bearer) return jsonResult({
|
|
3170
|
+
status: 401,
|
|
3171
|
+
error: {
|
|
3172
|
+
code: "MISSING_BEARER",
|
|
3173
|
+
message: "No developer access token available. Call amba_developer_signup or amba_developer_login first, then re-issue this MCP request with the returned access_token as the inbound Bearer."
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
return passthroughResult(await authFetch(apiClient, {
|
|
3177
|
+
method: "GET",
|
|
3178
|
+
path: "/auth/developer/me",
|
|
3179
|
+
bearer
|
|
3180
|
+
}));
|
|
3181
|
+
});
|
|
3182
|
+
}
|
|
3183
|
+
//#endregion
|
|
3184
|
+
//#region src/index.ts
|
|
3185
|
+
/**
|
|
3186
|
+
* Register every Amba MCP tool group against the given server, using the
|
|
3187
|
+
* given API client. A single `McpServer` instance should only have this
|
|
3188
|
+
* called against it once.
|
|
3189
|
+
*/
|
|
3190
|
+
function registerAllTools(server, apiClient) {
|
|
3191
|
+
registerTools(server, apiClient);
|
|
3192
|
+
registerTools$18(server, apiClient);
|
|
3193
|
+
registerTools$17(server, apiClient);
|
|
3194
|
+
registerTools$16(server, apiClient);
|
|
3195
|
+
registerTools$15(server, apiClient);
|
|
3196
|
+
registerTools$14(server, apiClient);
|
|
3197
|
+
registerTools$13(server, apiClient);
|
|
3198
|
+
registerTools$12(server, apiClient);
|
|
3199
|
+
registerTools$11(server, apiClient);
|
|
3200
|
+
registerTools$10(server, apiClient);
|
|
3201
|
+
registerTools$9(server, apiClient);
|
|
3202
|
+
registerTools$8(server, apiClient);
|
|
3203
|
+
registerTools$7(server, apiClient);
|
|
3204
|
+
registerTools$6(server, apiClient);
|
|
3205
|
+
registerTools$5(server, apiClient);
|
|
3206
|
+
registerTools$4(server, apiClient);
|
|
3207
|
+
registerTools$3(server, apiClient);
|
|
3208
|
+
registerTools$2(server, apiClient);
|
|
3209
|
+
registerTools$1(server, apiClient);
|
|
3210
|
+
}
|
|
3211
|
+
/**
|
|
3212
|
+
* Convenience factory used by the hosted MCP server to build a per-request
|
|
3213
|
+
* `ApiClient` whose Bearer token comes from the inbound HTTP `Authorization`
|
|
3214
|
+
* header. Equivalent to `new ApiClient({ baseUrl, getToken: async () => token })`.
|
|
3215
|
+
*
|
|
3216
|
+
* If `token` is omitted the client falls back to `~/.amba/credentials.json`
|
|
3217
|
+
* — that path is only useful for tests or local-dev scripts; production
|
|
3218
|
+
* hosted-MCP traffic always passes an explicit token.
|
|
3219
|
+
*/
|
|
3220
|
+
function createApiClient(options) {
|
|
3221
|
+
const opts = {};
|
|
3222
|
+
if (options.baseUrl !== void 0) opts.baseUrl = options.baseUrl;
|
|
3223
|
+
if (options.token !== void 0) {
|
|
3224
|
+
const token = options.token;
|
|
3225
|
+
opts.getToken = async () => token;
|
|
3226
|
+
}
|
|
3227
|
+
return new ApiClient(opts);
|
|
3228
|
+
}
|
|
3229
|
+
//#endregion
|
|
3230
|
+
export { ApiClient, createApiClient, registerAllTools };
|