@fload-ai/mcp 0.1.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -46
- package/dist/.tsbuildinfo +1 -0
- package/dist/api-client.d.ts +12 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +106 -0
- package/dist/api-client.js.map +7 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +2278 -0
- package/dist/bin.js.map +7 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1392 -440
- package/dist/index.js.map +4 -4
- package/dist/lib/format.d.ts +5 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/public-errors.d.ts +2 -0
- package/dist/public-errors.d.ts.map +1 -0
- package/dist/rate-limiter.d.ts +12 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/tools/actions.d.ts +66 -0
- package/dist/tools/actions.d.ts.map +1 -0
- package/dist/tools/ads.d.ts +35 -0
- package/dist/tools/ads.d.ts.map +1 -0
- package/dist/tools/agents.d.ts +149 -0
- package/dist/tools/agents.d.ts.map +1 -0
- package/dist/tools/analytics.d.ts +84 -0
- package/dist/tools/analytics.d.ts.map +1 -0
- package/dist/tools/anomalies.d.ts +107 -0
- package/dist/tools/anomalies.d.ts.map +1 -0
- package/dist/tools/apps.d.ts +49 -0
- package/dist/tools/apps.d.ts.map +1 -0
- package/dist/tools/aso.d.ts +144 -0
- package/dist/tools/aso.d.ts.map +1 -0
- package/dist/tools/chat.d.ts +67 -0
- package/dist/tools/chat.d.ts.map +1 -0
- package/dist/tools/dashboard.d.ts +17 -0
- package/dist/tools/dashboard.d.ts.map +1 -0
- package/dist/tools/forecasting.d.ts +32 -0
- package/dist/tools/forecasting.d.ts.map +1 -0
- package/dist/tools/growth.d.ts +43 -0
- package/dist/tools/growth.d.ts.map +1 -0
- package/dist/tools/index.d.ts +20 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +2166 -0
- package/dist/tools/index.js.map +7 -0
- package/dist/tools/reviews.d.ts +134 -0
- package/dist/tools/reviews.d.ts.map +1 -0
- package/package.json +28 -15
package/dist/index.js
CHANGED
|
@@ -1,37 +1,105 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// src/public-errors.ts
|
|
2
|
+
var INTERNAL_ERROR_PATTERNS = [
|
|
3
|
+
/\b(stack|trace|at\s+[\w.$<>]+\s*\()/i,
|
|
4
|
+
/\b(sql|postgres|drizzle|database|constraint|duplicate key|violates|relation)\b/i,
|
|
5
|
+
/\b(fetch|undici|socket|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|HTTP\s+\d{3})\b/i,
|
|
6
|
+
/\b(token|secret|api[_-]?key|authorization|bearer|cookie|password)\b/i,
|
|
7
|
+
/\b[a-z0-9]+_(secret|token|key)\b/i,
|
|
8
|
+
/\b(secret|token|key)[_-][a-z0-9]+\b/i,
|
|
9
|
+
/\b(scrap|browser|patchright|playwright|chromium|page\.|locator|selector)\b/i,
|
|
10
|
+
/\b(redis|bullmq|queue|job id|worker)\b/i
|
|
11
|
+
];
|
|
12
|
+
function looksInternal(message) {
|
|
13
|
+
return INTERNAL_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
14
|
+
}
|
|
15
|
+
function publicMcpErrorMessage(message, fallbackMessage = "Fload could not complete the request. Please try again.") {
|
|
16
|
+
const raw = message instanceof Error ? message.message : typeof message === "string" ? message : "";
|
|
17
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
18
|
+
if (!normalized || normalized.length > 180 || looksInternal(normalized)) {
|
|
19
|
+
return fallbackMessage;
|
|
20
|
+
}
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
5
23
|
|
|
6
|
-
// src/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function loadConfig() {
|
|
12
|
-
const apiKey = process.env.FLOAD_API_KEY;
|
|
13
|
-
const apiUrl = process.env.FLOAD_API_URL || "https://api.fload.com";
|
|
14
|
-
if (apiKey) {
|
|
15
|
-
return { apiKey, apiUrl };
|
|
24
|
+
// src/api-client.ts
|
|
25
|
+
var FloadApiClient = class {
|
|
26
|
+
constructor(baseUrl, apiKey) {
|
|
27
|
+
this.baseUrl = baseUrl;
|
|
28
|
+
this.apiKey = apiKey;
|
|
16
29
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
async request(path, options = {}) {
|
|
31
|
+
const url = `${this.baseUrl}${path}`;
|
|
32
|
+
const headers = {
|
|
33
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
34
|
+
...options.body ? { "Content-Type": "application/json" } : {},
|
|
35
|
+
...options.headers
|
|
36
|
+
};
|
|
37
|
+
const response = await fetch(url, {
|
|
38
|
+
...options,
|
|
39
|
+
headers
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
43
|
+
const safeError = publicMcpErrorMessage(
|
|
44
|
+
error.publicMessage || error.message || response.statusText,
|
|
45
|
+
response.status >= 500 ? "Fload could not complete the request. Please try again." : "Fload rejected the request. Please check access and try again."
|
|
46
|
+
);
|
|
47
|
+
throw new Error(safeError);
|
|
48
|
+
}
|
|
49
|
+
return response.json();
|
|
50
|
+
}
|
|
51
|
+
async get(path, params) {
|
|
52
|
+
const searchParams = new URLSearchParams();
|
|
53
|
+
if (params) {
|
|
54
|
+
for (const [key, value] of Object.entries(params)) {
|
|
55
|
+
if (value !== void 0)
|
|
56
|
+
searchParams.set(key, String(value));
|
|
26
57
|
}
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error("Failed to parse config file:", error);
|
|
29
58
|
}
|
|
59
|
+
const query = searchParams.toString();
|
|
60
|
+
return this.request(`${path}${query ? `?${query}` : ""}`);
|
|
30
61
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
62
|
+
async post(path, body) {
|
|
63
|
+
return this.request(path, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
body: body ? JSON.stringify(body) : void 0
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async patch(path, body) {
|
|
69
|
+
return this.request(path, {
|
|
70
|
+
method: "PATCH",
|
|
71
|
+
body: body ? JSON.stringify(body) : void 0
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async delete(path) {
|
|
75
|
+
return this.request(path, { method: "DELETE" });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* POST to a streaming endpoint (SSE / AI SDK UI-message stream) and
|
|
79
|
+
* return the raw response body as text. Callers parse the event lines.
|
|
80
|
+
*/
|
|
81
|
+
async postStream(path, body) {
|
|
82
|
+
const url = `${this.baseUrl}${path}`;
|
|
83
|
+
const response = await fetch(url, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
Accept: "text/event-stream"
|
|
89
|
+
},
|
|
90
|
+
body: body ? JSON.stringify(body) : void 0
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
94
|
+
const safeError = publicMcpErrorMessage(
|
|
95
|
+
error.publicMessage || error.message || response.statusText,
|
|
96
|
+
response.status >= 500 ? "Fload could not complete the request. Please try again." : "Fload rejected the request. Please check access and try again."
|
|
97
|
+
);
|
|
98
|
+
throw new Error(safeError);
|
|
99
|
+
}
|
|
100
|
+
return response.text();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
35
103
|
|
|
36
104
|
// src/server.ts
|
|
37
105
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -91,9 +159,12 @@ async function listApps(input, client) {
|
|
|
91
159
|
try {
|
|
92
160
|
const response = await client.get("/api/assets", {
|
|
93
161
|
limit: input.limit,
|
|
94
|
-
offset: 0
|
|
162
|
+
offset: 0,
|
|
163
|
+
// The listing hides assets without a valuation by default; "list all
|
|
164
|
+
// apps" must include apps that haven't been valued yet (FLO-734).
|
|
165
|
+
includeUnvalued: "true"
|
|
95
166
|
});
|
|
96
|
-
const apps = response.data
|
|
167
|
+
const apps = response.data ?? [];
|
|
97
168
|
const filtered = input.platform ? apps.filter((app) => {
|
|
98
169
|
if (input.platform === "ios")
|
|
99
170
|
return !!app.appleAppId;
|
|
@@ -137,8 +208,14 @@ async function getAppDetails(input, client) {
|
|
|
137
208
|
}
|
|
138
209
|
let assetId = input.assetId;
|
|
139
210
|
if (!assetId && input.bundleId) {
|
|
140
|
-
const allApps = await client.get("/api/assets", {
|
|
141
|
-
|
|
211
|
+
const allApps = await client.get("/api/assets", {
|
|
212
|
+
limit: 100,
|
|
213
|
+
offset: 0,
|
|
214
|
+
includeUnvalued: "true"
|
|
215
|
+
});
|
|
216
|
+
const match = (allApps.data ?? []).find(
|
|
217
|
+
(app2) => app2.bundleId === input.bundleId
|
|
218
|
+
);
|
|
142
219
|
if (!match) {
|
|
143
220
|
return {
|
|
144
221
|
content: [
|
|
@@ -152,8 +229,13 @@ async function getAppDetails(input, client) {
|
|
|
152
229
|
}
|
|
153
230
|
assetId = match.id;
|
|
154
231
|
}
|
|
155
|
-
const response = await client.get(
|
|
156
|
-
|
|
232
|
+
const response = await client.get(
|
|
233
|
+
`/api/assets/${assetId}`
|
|
234
|
+
);
|
|
235
|
+
const app = response.data;
|
|
236
|
+
if (!app) {
|
|
237
|
+
throw new Error("App details response did not include app data");
|
|
238
|
+
}
|
|
157
239
|
return {
|
|
158
240
|
content: [
|
|
159
241
|
{
|
|
@@ -199,12 +281,38 @@ var getReviewsSchema = z2.object({
|
|
|
199
281
|
limit: z2.number().int().min(1).max(200).default(50).describe("Maximum number of reviews to return"),
|
|
200
282
|
sortBy: z2.enum(["date", "rating"]).default("date").describe("Sort reviews by date or rating")
|
|
201
283
|
});
|
|
284
|
+
var generateReviewReplySchema = z2.object({
|
|
285
|
+
reviewId: z2.string().describe("The review UUID to generate a reply for"),
|
|
286
|
+
assetId: z2.string().uuid().describe("The app UUID the review belongs to")
|
|
287
|
+
});
|
|
288
|
+
var sendReviewReplySchema = z2.object({
|
|
289
|
+
reviewId: z2.string().describe("The review UUID to reply to"),
|
|
290
|
+
assetId: z2.string().uuid().describe("The app UUID the review belongs to"),
|
|
291
|
+
response: z2.string().describe("The reply text to send")
|
|
292
|
+
});
|
|
293
|
+
var translateReviewSchema = z2.object({
|
|
294
|
+
reviewId: z2.string().describe("The review UUID to translate"),
|
|
295
|
+
assetId: z2.string().uuid().describe("The app UUID the review belongs to"),
|
|
296
|
+
// The translate endpoint receives the review content in the request body
|
|
297
|
+
// (it does not re-fetch it). Pass the fields from a get_reviews result.
|
|
298
|
+
rating: z2.number().int().min(1).max(5).describe("The review star rating, from get_reviews"),
|
|
299
|
+
title: z2.string().default("").describe("The review title, from get_reviews"),
|
|
300
|
+
body: z2.string().describe("The review text to translate, from get_reviews"),
|
|
301
|
+
storeFront: z2.string().default("").describe('The review storefront (e.g. "SE"), from get_reviews'),
|
|
302
|
+
appVersionString: z2.string().default("").describe("The app version the review was left on, from get_reviews")
|
|
303
|
+
});
|
|
202
304
|
async function getReviews(input, client) {
|
|
203
305
|
try {
|
|
204
306
|
let assetId = input.assetId;
|
|
205
307
|
if (!assetId && input.bundleId) {
|
|
206
|
-
const allApps = await client.get("/api/assets", {
|
|
207
|
-
|
|
308
|
+
const allApps = await client.get("/api/assets", {
|
|
309
|
+
limit: 100,
|
|
310
|
+
offset: 0,
|
|
311
|
+
includeUnvalued: "true"
|
|
312
|
+
});
|
|
313
|
+
const match = (allApps.data ?? []).find(
|
|
314
|
+
(app) => app.bundleId === input.bundleId
|
|
315
|
+
);
|
|
208
316
|
if (!match) {
|
|
209
317
|
return {
|
|
210
318
|
content: [
|
|
@@ -218,12 +326,20 @@ async function getReviews(input, client) {
|
|
|
218
326
|
}
|
|
219
327
|
assetId = match.id;
|
|
220
328
|
}
|
|
329
|
+
if (!assetId) {
|
|
330
|
+
return {
|
|
331
|
+
content: [
|
|
332
|
+
{
|
|
333
|
+
type: "text",
|
|
334
|
+
text: "Provide an assetId (or bundleId) to fetch reviews for. Use list_apps to find app IDs."
|
|
335
|
+
}
|
|
336
|
+
],
|
|
337
|
+
isError: true
|
|
338
|
+
};
|
|
339
|
+
}
|
|
221
340
|
const params = {
|
|
222
|
-
limit: input.limit
|
|
223
|
-
sortBy: input.sortBy
|
|
341
|
+
limit: input.limit
|
|
224
342
|
};
|
|
225
|
-
if (assetId)
|
|
226
|
-
params.assetId = assetId;
|
|
227
343
|
if (input.platform)
|
|
228
344
|
params.platform = input.platform;
|
|
229
345
|
if (input.rating !== void 0)
|
|
@@ -234,13 +350,25 @@ async function getReviews(input, client) {
|
|
|
234
350
|
params.startDate = input.startDate;
|
|
235
351
|
if (input.endDate)
|
|
236
352
|
params.endDate = input.endDate;
|
|
237
|
-
const response = await client.get(
|
|
238
|
-
|
|
353
|
+
const response = await client.get(
|
|
354
|
+
`/api/assets/${assetId}/reviews`,
|
|
355
|
+
params
|
|
356
|
+
);
|
|
357
|
+
let reviews = response.data?.reviews ?? [];
|
|
358
|
+
if (input.sortBy === "rating") {
|
|
359
|
+
reviews = [...reviews].sort(
|
|
360
|
+
(a, b) => Number(b.rating ?? 0) - Number(a.rating ?? 0)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
const hasReply = (review) => !!(review.developerResponse || review.replySource || review.hasReply);
|
|
239
364
|
const summary = {
|
|
240
365
|
totalReviews: reviews.length,
|
|
241
|
-
averageRating: reviews.length > 0 ? (reviews.reduce((sum,
|
|
242
|
-
|
|
243
|
-
|
|
366
|
+
averageRating: reviews.length > 0 ? (reviews.reduce((sum, review) => {
|
|
367
|
+
const rating = Number(review.rating ?? 0);
|
|
368
|
+
return sum + (Number.isFinite(rating) ? rating : 0);
|
|
369
|
+
}, 0) / reviews.length).toFixed(2) : 0,
|
|
370
|
+
repliedCount: reviews.filter((review) => hasReply(review)).length,
|
|
371
|
+
unrepliedCount: reviews.filter((review) => !hasReply(review)).length
|
|
244
372
|
};
|
|
245
373
|
return {
|
|
246
374
|
content: [
|
|
@@ -248,19 +376,23 @@ async function getReviews(input, client) {
|
|
|
248
376
|
type: "text",
|
|
249
377
|
text: formatAsJson({
|
|
250
378
|
summary,
|
|
251
|
-
reviews: reviews.map((
|
|
252
|
-
id:
|
|
253
|
-
appId:
|
|
254
|
-
platform:
|
|
255
|
-
rating:
|
|
256
|
-
title:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
379
|
+
reviews: reviews.map((review) => ({
|
|
380
|
+
id: review.id,
|
|
381
|
+
appId: review.appId,
|
|
382
|
+
platform: review.platform,
|
|
383
|
+
rating: review.rating,
|
|
384
|
+
title: review.title,
|
|
385
|
+
// The handler exposes the review text as `review`.
|
|
386
|
+
body: review.review ?? review.body,
|
|
387
|
+
author: review.nickname || review.author,
|
|
388
|
+
// Epoch ms from the handler.
|
|
389
|
+
date: review.lastModified || review.date,
|
|
390
|
+
version: review.appVersionString || review.version,
|
|
391
|
+
storeFront: review.storeFront,
|
|
392
|
+
hasReply: hasReply(review),
|
|
393
|
+
replySource: review.replySource ?? null,
|
|
394
|
+
hasDraft: review.hasDraft ?? false,
|
|
395
|
+
reply: review.developerResponse ? typeof review.developerResponse === "object" ? review.developerResponse.response : review.developerResponse : review.reply || null
|
|
264
396
|
}))
|
|
265
397
|
})
|
|
266
398
|
}
|
|
@@ -278,6 +410,93 @@ async function getReviews(input, client) {
|
|
|
278
410
|
};
|
|
279
411
|
}
|
|
280
412
|
}
|
|
413
|
+
async function generateReviewReply(input, client) {
|
|
414
|
+
try {
|
|
415
|
+
const response = await client.post(
|
|
416
|
+
`/api/assets/${input.assetId}/reviews/${input.reviewId}/generate-reply`,
|
|
417
|
+
void 0
|
|
418
|
+
);
|
|
419
|
+
return {
|
|
420
|
+
content: [
|
|
421
|
+
{
|
|
422
|
+
type: "text",
|
|
423
|
+
text: formatAsJson(response)
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
};
|
|
427
|
+
} catch (error) {
|
|
428
|
+
return {
|
|
429
|
+
content: [
|
|
430
|
+
{
|
|
431
|
+
type: "text",
|
|
432
|
+
text: formatError(error)
|
|
433
|
+
}
|
|
434
|
+
],
|
|
435
|
+
isError: true
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async function sendReviewReply(input, client) {
|
|
440
|
+
try {
|
|
441
|
+
const response = await client.post(
|
|
442
|
+
`/api/assets/${input.assetId}/reviews/${input.reviewId}/respond`,
|
|
443
|
+
// The API handler reads `responseText` from the body.
|
|
444
|
+
{ responseText: input.response }
|
|
445
|
+
);
|
|
446
|
+
return {
|
|
447
|
+
content: [
|
|
448
|
+
{
|
|
449
|
+
type: "text",
|
|
450
|
+
text: formatAsJson(response)
|
|
451
|
+
}
|
|
452
|
+
]
|
|
453
|
+
};
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return {
|
|
456
|
+
content: [
|
|
457
|
+
{
|
|
458
|
+
type: "text",
|
|
459
|
+
text: formatError(error)
|
|
460
|
+
}
|
|
461
|
+
],
|
|
462
|
+
isError: true
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function translateReview(input, client) {
|
|
467
|
+
try {
|
|
468
|
+
const response = await client.post(
|
|
469
|
+
`/api/assets/${input.assetId}/reviews/${input.reviewId}/translate`,
|
|
470
|
+
// The translate endpoint expects the review content in the body.
|
|
471
|
+
{
|
|
472
|
+
rating: input.rating,
|
|
473
|
+
title: input.title,
|
|
474
|
+
review: input.body,
|
|
475
|
+
storeFront: input.storeFront,
|
|
476
|
+
appVersionString: input.appVersionString,
|
|
477
|
+
lastModified: Date.now()
|
|
478
|
+
}
|
|
479
|
+
);
|
|
480
|
+
return {
|
|
481
|
+
content: [
|
|
482
|
+
{
|
|
483
|
+
type: "text",
|
|
484
|
+
text: formatAsJson(response)
|
|
485
|
+
}
|
|
486
|
+
]
|
|
487
|
+
};
|
|
488
|
+
} catch (error) {
|
|
489
|
+
return {
|
|
490
|
+
content: [
|
|
491
|
+
{
|
|
492
|
+
type: "text",
|
|
493
|
+
text: formatError(error)
|
|
494
|
+
}
|
|
495
|
+
],
|
|
496
|
+
isError: true
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
281
500
|
|
|
282
501
|
// src/tools/analytics.ts
|
|
283
502
|
import { z as z3 } from "zod";
|
|
@@ -337,9 +556,11 @@ var DIMENSION_INFO = {
|
|
|
337
556
|
};
|
|
338
557
|
async function discoverMetrics(input, client) {
|
|
339
558
|
try {
|
|
340
|
-
const response = await client.get(
|
|
341
|
-
|
|
342
|
-
|
|
559
|
+
const response = await client.get(
|
|
560
|
+
`/api/assets/${input.assetId}/metrics/availability`
|
|
561
|
+
);
|
|
562
|
+
const availableMetrics = response.data?.availableMetrics ?? [];
|
|
563
|
+
const metricGroups = response.data?.metricGroups ?? {};
|
|
343
564
|
const enriched = availableMetrics.map((metricName) => {
|
|
344
565
|
const info = METRIC_INFO[metricName];
|
|
345
566
|
return {
|
|
@@ -390,10 +611,13 @@ async function getMetrics(input, client) {
|
|
|
390
611
|
if (input.dimensionFilter && input.dimension) {
|
|
391
612
|
params[`filter.${input.dimension}`] = input.dimensionFilter;
|
|
392
613
|
}
|
|
393
|
-
const response = await client.get(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
614
|
+
const response = await client.get(
|
|
615
|
+
`/api/assets/${input.assetId}/metrics/timeseries`,
|
|
616
|
+
params
|
|
617
|
+
);
|
|
618
|
+
const data = response.data ?? {};
|
|
619
|
+
const changes = response.changes ?? {};
|
|
620
|
+
const meta = response.meta ?? {};
|
|
397
621
|
const summaries = {};
|
|
398
622
|
for (const [metricName, timeseries] of Object.entries(data)) {
|
|
399
623
|
const points = timeseries;
|
|
@@ -449,17 +673,20 @@ async function discoverDimensions(input, client) {
|
|
|
449
673
|
assetId: input.assetId,
|
|
450
674
|
dimension: input.dimension,
|
|
451
675
|
displayName: DIMENSION_INFO[input.dimension] || input.dimension,
|
|
452
|
-
values: response2.data
|
|
676
|
+
values: response2.data ?? response2
|
|
453
677
|
})
|
|
454
678
|
}]
|
|
455
679
|
};
|
|
456
680
|
}
|
|
457
681
|
const response = await client.get(`/api/assets/${input.assetId}/metrics/dimensions`);
|
|
458
|
-
const dimensions = response.data
|
|
459
|
-
const enriched =
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
682
|
+
const dimensions = Array.isArray(response.data) ? response.data : [];
|
|
683
|
+
const enriched = dimensions.map((dim) => {
|
|
684
|
+
if (typeof dim === "string") {
|
|
685
|
+
return { name: dim, displayName: DIMENSION_INFO[dim] || dim };
|
|
686
|
+
}
|
|
687
|
+
const id = dim.id ?? "";
|
|
688
|
+
return { name: id, displayName: dim.label ?? DIMENSION_INFO[id] ?? id };
|
|
689
|
+
});
|
|
463
690
|
return {
|
|
464
691
|
content: [{
|
|
465
692
|
type: "text",
|
|
@@ -477,6 +704,9 @@ async function discoverDimensions(input, client) {
|
|
|
477
704
|
|
|
478
705
|
// src/tools/agents.ts
|
|
479
706
|
import { z as z4 } from "zod";
|
|
707
|
+
function asRecord(value) {
|
|
708
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
709
|
+
}
|
|
480
710
|
var AGENT_TYPES = [
|
|
481
711
|
"review",
|
|
482
712
|
"monitoring",
|
|
@@ -507,12 +737,25 @@ var getAgentRunHistorySchema = z4.object({
|
|
|
507
737
|
assetId: z4.string().uuid().optional().describe("App UUID to filter runs"),
|
|
508
738
|
limit: z4.number().int().min(1).max(100).default(20).describe("Maximum number of runs to return")
|
|
509
739
|
});
|
|
740
|
+
var triggerAgentRunSchema = z4.object({
|
|
741
|
+
agentId: z4.string().describe("The agent ID to trigger a run for"),
|
|
742
|
+
assetId: z4.string().uuid().optional().describe("Optional app UUID to run the agent against")
|
|
743
|
+
});
|
|
744
|
+
var pauseAgentSchema = z4.object({
|
|
745
|
+
agentId: z4.string().describe("The agent ID to pause")
|
|
746
|
+
});
|
|
747
|
+
var resumeAgentSchema = z4.object({
|
|
748
|
+
agentId: z4.string().describe("The agent ID to resume")
|
|
749
|
+
});
|
|
750
|
+
var getAgentActivitySchema = z4.object({
|
|
751
|
+
agentId: z4.string().describe("The agent ID to get activity for")
|
|
752
|
+
});
|
|
510
753
|
async function listAgents(_input, client) {
|
|
511
754
|
try {
|
|
512
755
|
let apiAgents = null;
|
|
513
756
|
try {
|
|
514
757
|
const response = await client.get("/api/agents");
|
|
515
|
-
apiAgents = response.data
|
|
758
|
+
apiAgents = Array.isArray(response) ? response : response.data ?? response.agents ?? null;
|
|
516
759
|
} catch {
|
|
517
760
|
}
|
|
518
761
|
if (apiAgents && Array.isArray(apiAgents)) {
|
|
@@ -562,21 +805,26 @@ async function getAgentDetails(input, client) {
|
|
|
562
805
|
const params = {};
|
|
563
806
|
if (input.assetId)
|
|
564
807
|
params.assetId = input.assetId;
|
|
565
|
-
const response2 = await client.get(`/api/agents/${input.agentType}`, params);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
808
|
+
const response2 = await client.get(`/api/agents/by-type/${input.agentType}`, params);
|
|
809
|
+
if (response2.agent) {
|
|
810
|
+
return {
|
|
811
|
+
content: [
|
|
812
|
+
{
|
|
813
|
+
type: "text",
|
|
814
|
+
text: formatAsJson({
|
|
815
|
+
agent: response2.agent,
|
|
816
|
+
recentRuns: response2.recentRuns ?? []
|
|
817
|
+
})
|
|
818
|
+
}
|
|
819
|
+
]
|
|
820
|
+
};
|
|
821
|
+
}
|
|
574
822
|
} catch {
|
|
575
823
|
}
|
|
576
824
|
const response = {
|
|
577
825
|
type: input.agentType,
|
|
578
826
|
description: AGENT_DESCRIPTIONS[input.agentType],
|
|
579
|
-
message:
|
|
827
|
+
message: `No ${input.agentType} agent is provisioned for this organization yet. Activate it from the Agents page or with trigger_agent_run once created.`
|
|
580
828
|
};
|
|
581
829
|
return {
|
|
582
830
|
content: [
|
|
@@ -600,33 +848,44 @@ async function getAgentDetails(input, client) {
|
|
|
600
848
|
}
|
|
601
849
|
async function getAgentRunHistory(input, client) {
|
|
602
850
|
try {
|
|
851
|
+
let agentId;
|
|
603
852
|
try {
|
|
604
|
-
const
|
|
605
|
-
limit: input.limit
|
|
606
|
-
};
|
|
853
|
+
const byTypeParams = {};
|
|
607
854
|
if (input.assetId)
|
|
608
|
-
|
|
609
|
-
const
|
|
855
|
+
byTypeParams.assetId = input.assetId;
|
|
856
|
+
const byType = await client.get(
|
|
857
|
+
`/api/agents/by-type/${input.agentType}`,
|
|
858
|
+
byTypeParams
|
|
859
|
+
);
|
|
860
|
+
agentId = byType.agent?.id;
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
if (!agentId) {
|
|
610
864
|
return {
|
|
611
865
|
content: [
|
|
612
866
|
{
|
|
613
867
|
type: "text",
|
|
614
868
|
text: formatAsJson({
|
|
615
869
|
agentType: input.agentType,
|
|
616
|
-
|
|
870
|
+
runs: [],
|
|
871
|
+
message: `No ${input.agentType} agent is provisioned for this organization yet, so there is no run history.`
|
|
617
872
|
})
|
|
618
873
|
}
|
|
619
874
|
]
|
|
620
875
|
};
|
|
621
|
-
} catch {
|
|
622
876
|
}
|
|
877
|
+
const response = await client.get(
|
|
878
|
+
`/api/agents/${agentId}/runs`,
|
|
879
|
+
{ limit: input.limit }
|
|
880
|
+
);
|
|
623
881
|
return {
|
|
624
882
|
content: [
|
|
625
883
|
{
|
|
626
884
|
type: "text",
|
|
627
885
|
text: formatAsJson({
|
|
628
886
|
agentType: input.agentType,
|
|
629
|
-
|
|
887
|
+
agentId,
|
|
888
|
+
...asRecord(response.data ?? response)
|
|
630
889
|
})
|
|
631
890
|
}
|
|
632
891
|
]
|
|
@@ -643,69 +902,18 @@ async function getAgentRunHistory(input, client) {
|
|
|
643
902
|
};
|
|
644
903
|
}
|
|
645
904
|
}
|
|
646
|
-
|
|
647
|
-
// src/tools/anomalies.ts
|
|
648
|
-
import { z as z5 } from "zod";
|
|
649
|
-
var getAnomaliesSchema = z5.object({
|
|
650
|
-
assetId: z5.string().uuid().optional().describe("Filter anomalies by app UUID"),
|
|
651
|
-
severity: z5.enum(["low", "medium", "high", "critical"]).optional().describe("Filter by severity level"),
|
|
652
|
-
type: z5.enum(["surge", "decline"]).optional().describe("Filter by anomaly type"),
|
|
653
|
-
status: z5.enum(["new", "viewed", "acknowledged", "dismissed"]).optional().describe("Filter by status"),
|
|
654
|
-
metricName: z5.string().optional().describe('Filter by metric name (e.g., "proceeds", "units")'),
|
|
655
|
-
fromDate: z5.string().optional().describe("Filter anomalies from this date (YYYY-MM-DD)"),
|
|
656
|
-
toDate: z5.string().optional().describe("Filter anomalies until this date (YYYY-MM-DD)"),
|
|
657
|
-
excludeDismissed: z5.boolean().default(true).describe("Exclude dismissed anomalies (default: true)"),
|
|
658
|
-
limit: z5.number().int().min(1).max(100).default(50).describe("Maximum number of anomalies to return")
|
|
659
|
-
});
|
|
660
|
-
async function getAnomalies(input, client) {
|
|
905
|
+
async function triggerAgentRun(input, client) {
|
|
661
906
|
try {
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
if (input.severity)
|
|
668
|
-
params.severity = input.severity;
|
|
669
|
-
if (input.type)
|
|
670
|
-
params.type = input.type;
|
|
671
|
-
if (input.status)
|
|
672
|
-
params.status = input.status;
|
|
673
|
-
if (input.metricName)
|
|
674
|
-
params.metricName = input.metricName;
|
|
675
|
-
if (input.fromDate)
|
|
676
|
-
params.fromDate = input.fromDate;
|
|
677
|
-
if (input.toDate)
|
|
678
|
-
params.toDate = input.toDate;
|
|
679
|
-
if (input.excludeDismissed)
|
|
680
|
-
params.excludeDismissed = input.excludeDismissed;
|
|
681
|
-
const response = await client.get("/api/anomalies", params);
|
|
682
|
-
const anomalies = response.data || response.anomalies || [];
|
|
907
|
+
const body = input.assetId ? { triggerContext: { assetId: input.assetId } } : {};
|
|
908
|
+
const response = await client.post(
|
|
909
|
+
`/api/agents/${input.agentId}/run`,
|
|
910
|
+
body
|
|
911
|
+
);
|
|
683
912
|
return {
|
|
684
913
|
content: [
|
|
685
914
|
{
|
|
686
915
|
type: "text",
|
|
687
|
-
text: formatAsJson(
|
|
688
|
-
total: anomalies.length,
|
|
689
|
-
anomalies: anomalies.map((a) => ({
|
|
690
|
-
id: a.id,
|
|
691
|
-
assetId: a.assetId,
|
|
692
|
-
assetName: a.assetName,
|
|
693
|
-
assetIcon: a.assetIcon,
|
|
694
|
-
anomalyDate: a.anomalyDate,
|
|
695
|
-
metricName: a.metricName,
|
|
696
|
-
sourceType: a.sourceType,
|
|
697
|
-
type: a.type,
|
|
698
|
-
severity: a.severity,
|
|
699
|
-
actualValue: parseFloat(a.actualValue || "0"),
|
|
700
|
-
expectedValue: parseFloat(a.expectedValue || "0"),
|
|
701
|
-
deviationPercent: parseFloat(a.deviationPercent || "0"),
|
|
702
|
-
confidence: parseFloat(a.confidence || "0"),
|
|
703
|
-
explanation: a.explanation,
|
|
704
|
-
suggestedActions: a.suggestedActions,
|
|
705
|
-
status: a.status,
|
|
706
|
-
detectedAt: a.detectedAt
|
|
707
|
-
}))
|
|
708
|
-
})
|
|
916
|
+
text: formatAsJson(response.data ?? response)
|
|
709
917
|
}
|
|
710
918
|
]
|
|
711
919
|
};
|
|
@@ -721,51 +929,16 @@ async function getAnomalies(input, client) {
|
|
|
721
929
|
};
|
|
722
930
|
}
|
|
723
931
|
}
|
|
724
|
-
|
|
725
|
-
// src/tools/ads.ts
|
|
726
|
-
import { z as z6 } from "zod";
|
|
727
|
-
var getAdsPerformanceSchema = z6.object({
|
|
728
|
-
assetId: z6.string().uuid().optional().describe("Filter by app UUID"),
|
|
729
|
-
platform: z6.enum(["apple_search_ads", "google_ads", "meta_ads", "tiktok_ads"]).optional().describe("Filter by ad platform"),
|
|
730
|
-
fromDate: z6.string().optional().describe("Start date for performance data (YYYY-MM-DD)"),
|
|
731
|
-
toDate: z6.string().optional().describe("End date for performance data (YYYY-MM-DD)"),
|
|
732
|
-
limit: z6.number().int().min(1).max(100).default(50).describe("Maximum number of campaigns to return")
|
|
733
|
-
});
|
|
734
|
-
async function getAdsPerformance(input, client) {
|
|
932
|
+
async function pauseAgent(input, client) {
|
|
735
933
|
try {
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if (input.assetId)
|
|
740
|
-
params.assetId = input.assetId;
|
|
741
|
-
if (input.platform)
|
|
742
|
-
params.platform = input.platform;
|
|
743
|
-
if (input.fromDate)
|
|
744
|
-
params.fromDate = input.fromDate;
|
|
745
|
-
if (input.toDate)
|
|
746
|
-
params.toDate = input.toDate;
|
|
747
|
-
const response = await client.get("/api/ads/campaigns", params);
|
|
748
|
-
const campaigns = response.data || response.campaigns || [];
|
|
934
|
+
const response = await client.post(
|
|
935
|
+
`/api/agents/${input.agentId}/pause`
|
|
936
|
+
);
|
|
749
937
|
return {
|
|
750
938
|
content: [
|
|
751
939
|
{
|
|
752
940
|
type: "text",
|
|
753
|
-
text: formatAsJson(
|
|
754
|
-
total: campaigns.length,
|
|
755
|
-
campaigns: campaigns.map((c) => ({
|
|
756
|
-
id: c.id,
|
|
757
|
-
assetId: c.assetId,
|
|
758
|
-
assetName: c.assetName,
|
|
759
|
-
platformCampaignId: c.platformCampaignId,
|
|
760
|
-
name: c.name,
|
|
761
|
-
status: c.status,
|
|
762
|
-
objective: c.objective,
|
|
763
|
-
platform: c.platform,
|
|
764
|
-
linkSource: c.linkSource,
|
|
765
|
-
linkedAt: c.linkedAt,
|
|
766
|
-
recentPerformance: c.recentPerformance || null
|
|
767
|
-
}))
|
|
768
|
-
})
|
|
941
|
+
text: formatAsJson(response.data ?? response)
|
|
769
942
|
}
|
|
770
943
|
]
|
|
771
944
|
};
|
|
@@ -781,7 +954,288 @@ async function getAdsPerformance(input, client) {
|
|
|
781
954
|
};
|
|
782
955
|
}
|
|
783
956
|
}
|
|
784
|
-
|
|
957
|
+
async function resumeAgent(input, client) {
|
|
958
|
+
try {
|
|
959
|
+
const response = await client.post(
|
|
960
|
+
`/api/agents/${input.agentId}/resume`
|
|
961
|
+
);
|
|
962
|
+
return {
|
|
963
|
+
content: [
|
|
964
|
+
{
|
|
965
|
+
type: "text",
|
|
966
|
+
text: formatAsJson(response.data ?? response)
|
|
967
|
+
}
|
|
968
|
+
]
|
|
969
|
+
};
|
|
970
|
+
} catch (error) {
|
|
971
|
+
return {
|
|
972
|
+
content: [
|
|
973
|
+
{
|
|
974
|
+
type: "text",
|
|
975
|
+
text: formatError(error)
|
|
976
|
+
}
|
|
977
|
+
],
|
|
978
|
+
isError: true
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
async function getAgentActivity(input, client) {
|
|
983
|
+
try {
|
|
984
|
+
const response = await client.get(
|
|
985
|
+
`/api/agents/${input.agentId}/activity`
|
|
986
|
+
);
|
|
987
|
+
return {
|
|
988
|
+
content: [
|
|
989
|
+
{
|
|
990
|
+
type: "text",
|
|
991
|
+
text: formatAsJson(response.data ?? response)
|
|
992
|
+
}
|
|
993
|
+
]
|
|
994
|
+
};
|
|
995
|
+
} catch (error) {
|
|
996
|
+
return {
|
|
997
|
+
content: [
|
|
998
|
+
{
|
|
999
|
+
type: "text",
|
|
1000
|
+
text: formatError(error)
|
|
1001
|
+
}
|
|
1002
|
+
],
|
|
1003
|
+
isError: true
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/tools/anomalies.ts
|
|
1009
|
+
import { z as z5 } from "zod";
|
|
1010
|
+
var getAnomaliesSchema = z5.object({
|
|
1011
|
+
assetId: z5.string().uuid().optional().describe("Filter anomalies by app UUID"),
|
|
1012
|
+
severity: z5.enum(["low", "medium", "high", "critical"]).optional().describe("Filter by severity level"),
|
|
1013
|
+
type: z5.enum(["surge", "decline"]).optional().describe("Filter by anomaly type"),
|
|
1014
|
+
status: z5.enum(["new", "viewed", "acknowledged", "dismissed"]).optional().describe("Filter by status"),
|
|
1015
|
+
metricName: z5.string().optional().describe('Filter by metric name (e.g., "proceeds", "units")'),
|
|
1016
|
+
fromDate: z5.string().optional().describe("Filter anomalies from this date (YYYY-MM-DD)"),
|
|
1017
|
+
toDate: z5.string().optional().describe("Filter anomalies until this date (YYYY-MM-DD)"),
|
|
1018
|
+
excludeDismissed: z5.boolean().default(true).describe("Exclude dismissed anomalies (default: true)"),
|
|
1019
|
+
limit: z5.number().int().min(1).max(100).default(50).describe("Maximum number of anomalies to return")
|
|
1020
|
+
});
|
|
1021
|
+
var getAnomalyDetailSchema = z5.object({
|
|
1022
|
+
id: z5.string().uuid().describe("The anomaly UUID to get details for")
|
|
1023
|
+
});
|
|
1024
|
+
var acknowledgeAnomalySchema = z5.object({
|
|
1025
|
+
id: z5.string().uuid().describe("The anomaly UUID to acknowledge")
|
|
1026
|
+
});
|
|
1027
|
+
var dismissAnomalySchema = z5.object({
|
|
1028
|
+
id: z5.string().uuid().describe("The anomaly UUID to dismiss")
|
|
1029
|
+
});
|
|
1030
|
+
async function getAnomalies(input, client) {
|
|
1031
|
+
try {
|
|
1032
|
+
const params = {
|
|
1033
|
+
limit: input.limit
|
|
1034
|
+
};
|
|
1035
|
+
if (input.assetId)
|
|
1036
|
+
params.assetId = input.assetId;
|
|
1037
|
+
if (input.severity)
|
|
1038
|
+
params.severity = input.severity;
|
|
1039
|
+
if (input.type)
|
|
1040
|
+
params.type = input.type;
|
|
1041
|
+
if (input.status)
|
|
1042
|
+
params.status = input.status;
|
|
1043
|
+
if (input.metricName)
|
|
1044
|
+
params.metricName = input.metricName;
|
|
1045
|
+
if (input.fromDate)
|
|
1046
|
+
params.fromDate = input.fromDate;
|
|
1047
|
+
if (input.toDate)
|
|
1048
|
+
params.toDate = input.toDate;
|
|
1049
|
+
const response = await client.get(
|
|
1050
|
+
"/api/anomalies",
|
|
1051
|
+
params
|
|
1052
|
+
);
|
|
1053
|
+
let anomalies = response.data ?? response.anomalies ?? [];
|
|
1054
|
+
if (input.excludeDismissed && !input.status) {
|
|
1055
|
+
anomalies = anomalies.filter((a) => a.status !== "dismissed");
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
content: [
|
|
1059
|
+
{
|
|
1060
|
+
type: "text",
|
|
1061
|
+
text: formatAsJson({
|
|
1062
|
+
total: anomalies.length,
|
|
1063
|
+
anomalies: anomalies.map((anomaly) => ({
|
|
1064
|
+
id: anomaly.id,
|
|
1065
|
+
assetId: anomaly.assetId,
|
|
1066
|
+
assetName: anomaly.assetName,
|
|
1067
|
+
assetIcon: anomaly.assetIcon,
|
|
1068
|
+
anomalyDate: anomaly.anomalyDate,
|
|
1069
|
+
metricName: anomaly.metricName,
|
|
1070
|
+
sourceType: anomaly.sourceType,
|
|
1071
|
+
type: anomaly.type,
|
|
1072
|
+
severity: anomaly.severity,
|
|
1073
|
+
actualValue: parseFloat(String(anomaly.actualValue || "0")),
|
|
1074
|
+
expectedValue: parseFloat(String(anomaly.expectedValue || "0")),
|
|
1075
|
+
deviationPercent: parseFloat(
|
|
1076
|
+
String(anomaly.deviationPercent || "0")
|
|
1077
|
+
),
|
|
1078
|
+
confidence: parseFloat(String(anomaly.confidence || "0")),
|
|
1079
|
+
explanation: anomaly.explanation,
|
|
1080
|
+
suggestedActions: anomaly.suggestedActions,
|
|
1081
|
+
status: anomaly.status,
|
|
1082
|
+
detectedAt: anomaly.detectedAt
|
|
1083
|
+
}))
|
|
1084
|
+
})
|
|
1085
|
+
}
|
|
1086
|
+
]
|
|
1087
|
+
};
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
return {
|
|
1090
|
+
content: [
|
|
1091
|
+
{
|
|
1092
|
+
type: "text",
|
|
1093
|
+
text: formatError(error)
|
|
1094
|
+
}
|
|
1095
|
+
],
|
|
1096
|
+
isError: true
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
async function getAnomalyDetail(input, client) {
|
|
1101
|
+
try {
|
|
1102
|
+
const response = await client.get(`/api/anomalies/${input.id}`);
|
|
1103
|
+
return {
|
|
1104
|
+
content: [
|
|
1105
|
+
{
|
|
1106
|
+
type: "text",
|
|
1107
|
+
text: formatAsJson(response)
|
|
1108
|
+
}
|
|
1109
|
+
]
|
|
1110
|
+
};
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
return {
|
|
1113
|
+
content: [
|
|
1114
|
+
{
|
|
1115
|
+
type: "text",
|
|
1116
|
+
text: formatError(error)
|
|
1117
|
+
}
|
|
1118
|
+
],
|
|
1119
|
+
isError: true
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
async function acknowledgeAnomaly(input, client) {
|
|
1124
|
+
try {
|
|
1125
|
+
const response = await client.patch(`/api/anomalies/${input.id}/acknowledge`);
|
|
1126
|
+
return {
|
|
1127
|
+
content: [
|
|
1128
|
+
{
|
|
1129
|
+
type: "text",
|
|
1130
|
+
text: formatAsJson(response)
|
|
1131
|
+
}
|
|
1132
|
+
]
|
|
1133
|
+
};
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
return {
|
|
1136
|
+
content: [
|
|
1137
|
+
{
|
|
1138
|
+
type: "text",
|
|
1139
|
+
text: formatError(error)
|
|
1140
|
+
}
|
|
1141
|
+
],
|
|
1142
|
+
isError: true
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
async function dismissAnomaly(input, client) {
|
|
1147
|
+
try {
|
|
1148
|
+
const response = await client.patch(`/api/anomalies/${input.id}/dismiss`);
|
|
1149
|
+
return {
|
|
1150
|
+
content: [
|
|
1151
|
+
{
|
|
1152
|
+
type: "text",
|
|
1153
|
+
text: formatAsJson(response)
|
|
1154
|
+
}
|
|
1155
|
+
]
|
|
1156
|
+
};
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
return {
|
|
1159
|
+
content: [
|
|
1160
|
+
{
|
|
1161
|
+
type: "text",
|
|
1162
|
+
text: formatError(error)
|
|
1163
|
+
}
|
|
1164
|
+
],
|
|
1165
|
+
isError: true
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// src/tools/ads.ts
|
|
1171
|
+
import { z as z6 } from "zod";
|
|
1172
|
+
var getAdsPerformanceSchema = z6.object({
|
|
1173
|
+
assetId: z6.string().uuid().optional().describe("Filter by app UUID"),
|
|
1174
|
+
platform: z6.enum(["apple_search_ads", "google_ads", "meta_ads", "tiktok_ads"]).optional().describe("Filter by ad platform"),
|
|
1175
|
+
fromDate: z6.string().optional().describe("Start date for performance data (YYYY-MM-DD)"),
|
|
1176
|
+
toDate: z6.string().optional().describe("End date for performance data (YYYY-MM-DD)"),
|
|
1177
|
+
limit: z6.number().int().min(1).max(100).default(50).describe("Maximum number of campaigns to return")
|
|
1178
|
+
});
|
|
1179
|
+
async function getAdsPerformance(input, client) {
|
|
1180
|
+
try {
|
|
1181
|
+
const params = {};
|
|
1182
|
+
if (input.assetId)
|
|
1183
|
+
params.assetId = input.assetId;
|
|
1184
|
+
if (input.fromDate)
|
|
1185
|
+
params.from = input.fromDate;
|
|
1186
|
+
if (input.toDate)
|
|
1187
|
+
params.to = input.toDate;
|
|
1188
|
+
const response = await client.get(
|
|
1189
|
+
"/api/ads-agent/campaigns",
|
|
1190
|
+
params
|
|
1191
|
+
);
|
|
1192
|
+
let campaigns = response.data ?? response.campaigns ?? [];
|
|
1193
|
+
if (input.platform) {
|
|
1194
|
+
campaigns = campaigns.filter((c) => c.platform === input.platform);
|
|
1195
|
+
}
|
|
1196
|
+
campaigns = campaigns.slice(0, input.limit);
|
|
1197
|
+
return {
|
|
1198
|
+
content: [
|
|
1199
|
+
{
|
|
1200
|
+
type: "text",
|
|
1201
|
+
text: formatAsJson({
|
|
1202
|
+
total: campaigns.length,
|
|
1203
|
+
campaigns: campaigns.map((campaign) => ({
|
|
1204
|
+
id: campaign.id,
|
|
1205
|
+
assetId: campaign.assetId,
|
|
1206
|
+
assetName: campaign.assetName,
|
|
1207
|
+
platformCampaignId: campaign.platformCampaignId,
|
|
1208
|
+
name: campaign.name,
|
|
1209
|
+
status: campaign.status,
|
|
1210
|
+
objective: campaign.objective,
|
|
1211
|
+
platform: campaign.platform,
|
|
1212
|
+
linkSource: campaign.linkSource,
|
|
1213
|
+
linkedAt: campaign.linkedAt,
|
|
1214
|
+
totalSpend: campaign.totalSpend ?? null,
|
|
1215
|
+
totalImpressions: campaign.totalImpressions ?? null,
|
|
1216
|
+
totalTaps: campaign.totalTaps ?? null,
|
|
1217
|
+
totalInstalls: campaign.totalInstalls ?? null,
|
|
1218
|
+
avgCPI: campaign.avgCPI ?? null,
|
|
1219
|
+
avgTTR: campaign.avgTTR ?? null,
|
|
1220
|
+
avgCVR: campaign.avgCVR ?? null
|
|
1221
|
+
}))
|
|
1222
|
+
})
|
|
1223
|
+
}
|
|
1224
|
+
]
|
|
1225
|
+
};
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
return {
|
|
1228
|
+
content: [
|
|
1229
|
+
{
|
|
1230
|
+
type: "text",
|
|
1231
|
+
text: formatError(error)
|
|
1232
|
+
}
|
|
1233
|
+
],
|
|
1234
|
+
isError: true
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
785
1239
|
// src/tools/growth.ts
|
|
786
1240
|
import { z as z7 } from "zod";
|
|
787
1241
|
var getGrowthAuditSchema = z7.object({
|
|
@@ -841,13 +1295,17 @@ async function getGrowthScore(input, client) {
|
|
|
841
1295
|
import { z as z8 } from "zod";
|
|
842
1296
|
var getForecastsSchema = z8.object({
|
|
843
1297
|
assetId: z8.string().uuid().describe("App UUID to get forecasts for"),
|
|
844
|
-
|
|
1298
|
+
metric: z8.enum(["proceeds", "total_revenue", "total_downloads", "active_subs", "new_trials"]).describe("The metric to forecast"),
|
|
1299
|
+
forecastWeeks: z8.number().int().min(4).max(156).optional().describe("Weeks to forecast forward (default 104)"),
|
|
1300
|
+
historicalWeeks: z8.number().int().min(12).max(156).optional().describe("Weeks of history to base the forecast on (default 104)")
|
|
845
1301
|
});
|
|
846
1302
|
async function getForecasts(input, client) {
|
|
847
1303
|
try {
|
|
848
1304
|
const response = await client.get("/api/forecasting/forecast", {
|
|
849
1305
|
assetId: input.assetId,
|
|
850
|
-
|
|
1306
|
+
metricName: input.metric,
|
|
1307
|
+
forecastWeeks: input.forecastWeeks,
|
|
1308
|
+
historicalWeeks: input.historicalWeeks
|
|
851
1309
|
});
|
|
852
1310
|
return {
|
|
853
1311
|
content: [
|
|
@@ -872,18 +1330,34 @@ async function getForecasts(input, client) {
|
|
|
872
1330
|
|
|
873
1331
|
// src/tools/dashboard.ts
|
|
874
1332
|
import { z as z9 } from "zod";
|
|
1333
|
+
function parseDashboardNumber(value) {
|
|
1334
|
+
if (value === null || value === void 0 || value === "")
|
|
1335
|
+
return null;
|
|
1336
|
+
const parsed = typeof value === "number" ? value : Number.parseFloat(value);
|
|
1337
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1338
|
+
}
|
|
1339
|
+
function getSidebarAssets(response) {
|
|
1340
|
+
if (!response)
|
|
1341
|
+
return [];
|
|
1342
|
+
if (Array.isArray(response))
|
|
1343
|
+
return response;
|
|
1344
|
+
return response.data ?? [];
|
|
1345
|
+
}
|
|
875
1346
|
var getDashboardOverviewSchema = z9.object({});
|
|
876
1347
|
async function getDashboardOverview(_input, client) {
|
|
877
1348
|
try {
|
|
1349
|
+
const overviewPromise = client.get("/api/dashboard/overview-metrics").catch(() => null);
|
|
1350
|
+
const sidebarPromise = client.get(
|
|
1351
|
+
"/api/dashboard/sidebar-assets"
|
|
1352
|
+
).catch(() => null);
|
|
878
1353
|
const [overviewResponse, sidebarResponse] = await Promise.all([
|
|
879
|
-
|
|
880
|
-
|
|
1354
|
+
overviewPromise,
|
|
1355
|
+
sidebarPromise
|
|
881
1356
|
]);
|
|
882
|
-
const overview = overviewResponse?.data
|
|
883
|
-
const
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
return sum + parseFloat(a.currentValuation || "0");
|
|
1357
|
+
const overview = overviewResponse?.data ?? overviewResponse ?? {};
|
|
1358
|
+
const apps = getSidebarAssets(sidebarResponse);
|
|
1359
|
+
const totalValuation = apps.reduce((sum, app) => {
|
|
1360
|
+
return sum + (parseDashboardNumber(app.currentValuation) ?? 0);
|
|
887
1361
|
}, 0);
|
|
888
1362
|
return {
|
|
889
1363
|
content: [
|
|
@@ -893,16 +1367,16 @@ async function getDashboardOverview(_input, client) {
|
|
|
893
1367
|
portfolio: {
|
|
894
1368
|
totalApps: apps.length,
|
|
895
1369
|
totalValuation: totalValuation > 0 ? totalValuation : null,
|
|
896
|
-
apps: apps.map((
|
|
897
|
-
id:
|
|
898
|
-
name:
|
|
899
|
-
bundleId:
|
|
900
|
-
platform:
|
|
901
|
-
currentValuation:
|
|
902
|
-
rating:
|
|
903
|
-
ratingCount:
|
|
904
|
-
category:
|
|
905
|
-
iconUrl:
|
|
1370
|
+
apps: apps.map((app) => ({
|
|
1371
|
+
id: app.id,
|
|
1372
|
+
name: app.name,
|
|
1373
|
+
bundleId: app.bundleId,
|
|
1374
|
+
platform: app.appleAppId ? "ios" : app.googleAppId ? "android" : "unknown",
|
|
1375
|
+
currentValuation: parseDashboardNumber(app.currentValuation),
|
|
1376
|
+
rating: parseDashboardNumber(app.rating),
|
|
1377
|
+
ratingCount: app.ratingCount ?? null,
|
|
1378
|
+
category: app.category ?? null,
|
|
1379
|
+
iconUrl: app.iconUrl ?? null
|
|
906
1380
|
}))
|
|
907
1381
|
},
|
|
908
1382
|
overview
|
|
@@ -925,13 +1399,15 @@ async function getDashboardOverview(_input, client) {
|
|
|
925
1399
|
|
|
926
1400
|
// src/tools/actions.ts
|
|
927
1401
|
import { z as z10 } from "zod";
|
|
1402
|
+
function asRecord2(value) {
|
|
1403
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1404
|
+
}
|
|
928
1405
|
var listPendingActionsSchema = z10.object({
|
|
929
1406
|
assetId: z10.string().uuid().optional().describe("Filter by app UUID"),
|
|
930
1407
|
limit: z10.number().int().min(1).max(100).default(50).describe("Maximum number of actions to return")
|
|
931
1408
|
});
|
|
932
1409
|
var approveActionSchema = z10.object({
|
|
933
|
-
actionId: z10.string().describe("The
|
|
934
|
-
editedReply: z10.string().optional().describe("Optionally modify the reply text before approving")
|
|
1410
|
+
actionId: z10.string().describe("The pending action ID to approve")
|
|
935
1411
|
});
|
|
936
1412
|
var rejectActionSchema = z10.object({
|
|
937
1413
|
actionId: z10.string().describe("The draft reply ID to reject/delete")
|
|
@@ -941,10 +1417,16 @@ async function listPendingActions(input, client) {
|
|
|
941
1417
|
const params = {
|
|
942
1418
|
limit: input.limit
|
|
943
1419
|
};
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1420
|
+
const response = await client.get(
|
|
1421
|
+
"/api/pending-actions",
|
|
1422
|
+
params
|
|
1423
|
+
);
|
|
1424
|
+
let actions = response.data ?? response.actions ?? [];
|
|
1425
|
+
if (input.assetId) {
|
|
1426
|
+
actions = actions.filter(
|
|
1427
|
+
(action) => action.assetId === input.assetId
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
948
1430
|
return {
|
|
949
1431
|
content: [
|
|
950
1432
|
{
|
|
@@ -970,10 +1452,10 @@ async function listPendingActions(input, client) {
|
|
|
970
1452
|
}
|
|
971
1453
|
async function approveAction(input, client) {
|
|
972
1454
|
try {
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1455
|
+
const response = await client.post(
|
|
1456
|
+
`/api/pending-actions/${input.actionId}/approve`,
|
|
1457
|
+
{}
|
|
1458
|
+
);
|
|
977
1459
|
return {
|
|
978
1460
|
content: [
|
|
979
1461
|
{
|
|
@@ -983,7 +1465,7 @@ async function approveAction(input, client) {
|
|
|
983
1465
|
actionId: input.actionId,
|
|
984
1466
|
status: "approved",
|
|
985
1467
|
message: "Reply has been approved and queued for sending",
|
|
986
|
-
...response.data
|
|
1468
|
+
...asRecord2(response.data)
|
|
987
1469
|
})
|
|
988
1470
|
}
|
|
989
1471
|
]
|
|
@@ -1002,7 +1484,9 @@ async function approveAction(input, client) {
|
|
|
1002
1484
|
}
|
|
1003
1485
|
async function rejectAction(input, client) {
|
|
1004
1486
|
try {
|
|
1005
|
-
const response = await client.post(
|
|
1487
|
+
const response = await client.post(
|
|
1488
|
+
`/api/pending-actions/${input.actionId}/reject`
|
|
1489
|
+
);
|
|
1006
1490
|
return {
|
|
1007
1491
|
content: [
|
|
1008
1492
|
{
|
|
@@ -1012,7 +1496,7 @@ async function rejectAction(input, client) {
|
|
|
1012
1496
|
actionId: input.actionId,
|
|
1013
1497
|
status: "rejected",
|
|
1014
1498
|
message: "Draft reply has been rejected and removed",
|
|
1015
|
-
...response.data
|
|
1499
|
+
...asRecord2(response.data)
|
|
1016
1500
|
})
|
|
1017
1501
|
}
|
|
1018
1502
|
]
|
|
@@ -1030,262 +1514,730 @@ async function rejectAction(input, client) {
|
|
|
1030
1514
|
}
|
|
1031
1515
|
}
|
|
1032
1516
|
|
|
1033
|
-
// src/
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1517
|
+
// src/tools/aso.ts
|
|
1518
|
+
import { z as z11 } from "zod";
|
|
1519
|
+
var getAsoSummarySchema = z11.object({
|
|
1520
|
+
assetId: z11.string().uuid().describe("App UUID to get ASO summary for")
|
|
1521
|
+
});
|
|
1522
|
+
var getAsoRecommendationsSchema = z11.object({
|
|
1523
|
+
assetId: z11.string().uuid().describe("App UUID to get ASO recommendations for")
|
|
1524
|
+
});
|
|
1525
|
+
var getAsoKeywordsSchema = z11.object({
|
|
1526
|
+
assetId: z11.string().uuid().describe("App UUID to get keyword intelligence for"),
|
|
1527
|
+
locale: z11.string().optional().describe('Locale code to filter keywords (e.g., "en-US", "de-DE"). Returns all locales if omitted.')
|
|
1528
|
+
});
|
|
1529
|
+
var getAsoExperimentsSchema = z11.object({
|
|
1530
|
+
assetId: z11.string().uuid().describe("App UUID to list ASO experiments for"),
|
|
1531
|
+
status: z11.enum(["proposed", "approved", "applied", "measuring", "completed", "reverted"]).optional().describe("Filter experiments by status"),
|
|
1532
|
+
limit: z11.number().int().min(1).max(100).default(20).describe("Maximum number of experiments to return"),
|
|
1533
|
+
cursor: z11.string().optional().describe("Pagination cursor from a previous response (meta.nextCursor)")
|
|
1534
|
+
});
|
|
1535
|
+
var getAsoLocaleSnapshotsSchema = z11.object({
|
|
1536
|
+
assetId: z11.string().uuid().describe("App UUID to get locale snapshots for"),
|
|
1537
|
+
store: z11.enum(["ios", "android"]).default("ios").describe("Which store listing to read"),
|
|
1538
|
+
locale: z11.string().optional().describe('Filter to a single locale (e.g. "en-US"). Returns all locales if omitted.'),
|
|
1539
|
+
limit: z11.number().int().min(1).max(500).optional().describe("Maximum snapshot rows to return (newest first)")
|
|
1540
|
+
});
|
|
1541
|
+
var triggerAsoAnalysisSchema = z11.object({
|
|
1542
|
+
assetId: z11.string().uuid().describe("App UUID to trigger ASO analysis for")
|
|
1543
|
+
});
|
|
1544
|
+
async function getAsoSummary(input, client) {
|
|
1545
|
+
try {
|
|
1546
|
+
const response = await client.get(`/api/assets/${input.assetId}/aso/summary`);
|
|
1547
|
+
return {
|
|
1548
|
+
content: [
|
|
1549
|
+
{
|
|
1550
|
+
type: "text",
|
|
1551
|
+
text: formatAsJson(response)
|
|
1552
|
+
}
|
|
1553
|
+
]
|
|
1554
|
+
};
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
return {
|
|
1557
|
+
content: [
|
|
1558
|
+
{
|
|
1559
|
+
type: "text",
|
|
1560
|
+
text: formatError(error)
|
|
1561
|
+
}
|
|
1562
|
+
],
|
|
1563
|
+
isError: true
|
|
1054
1564
|
};
|
|
1055
1565
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
{
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1566
|
+
}
|
|
1567
|
+
async function getAsoRecommendations(input, client) {
|
|
1568
|
+
try {
|
|
1569
|
+
const response = await client.get(`/api/assets/${input.assetId}/aso/recommendations`);
|
|
1570
|
+
return {
|
|
1571
|
+
content: [
|
|
1572
|
+
{
|
|
1573
|
+
type: "text",
|
|
1574
|
+
text: formatAsJson(response)
|
|
1575
|
+
}
|
|
1576
|
+
]
|
|
1577
|
+
};
|
|
1578
|
+
} catch (error) {
|
|
1579
|
+
return {
|
|
1580
|
+
content: [
|
|
1581
|
+
{
|
|
1582
|
+
type: "text",
|
|
1583
|
+
text: formatError(error)
|
|
1584
|
+
}
|
|
1585
|
+
],
|
|
1586
|
+
isError: true
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
async function getAsoKeywords(input, client) {
|
|
1591
|
+
try {
|
|
1592
|
+
const params = {};
|
|
1593
|
+
if (input.locale)
|
|
1594
|
+
params.locale = input.locale;
|
|
1595
|
+
const response = await client.get(`/api/assets/${input.assetId}/aso/keyword-intelligence`, params);
|
|
1596
|
+
return {
|
|
1597
|
+
content: [
|
|
1598
|
+
{
|
|
1599
|
+
type: "text",
|
|
1600
|
+
text: formatAsJson(response)
|
|
1601
|
+
}
|
|
1602
|
+
]
|
|
1603
|
+
};
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
return {
|
|
1606
|
+
content: [
|
|
1607
|
+
{
|
|
1608
|
+
type: "text",
|
|
1609
|
+
text: formatError(error)
|
|
1610
|
+
}
|
|
1611
|
+
],
|
|
1612
|
+
isError: true
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
async function getAsoExperiments(input, client) {
|
|
1617
|
+
try {
|
|
1618
|
+
const params = {
|
|
1619
|
+
limit: input.limit,
|
|
1620
|
+
cursor: input.cursor
|
|
1621
|
+
};
|
|
1622
|
+
if (input.status)
|
|
1623
|
+
params.status = input.status;
|
|
1624
|
+
const response = await client.get(`/api/assets/${input.assetId}/aso/experiments`, params);
|
|
1625
|
+
return {
|
|
1626
|
+
content: [
|
|
1627
|
+
{
|
|
1628
|
+
type: "text",
|
|
1629
|
+
text: formatAsJson(response)
|
|
1630
|
+
}
|
|
1631
|
+
]
|
|
1632
|
+
};
|
|
1633
|
+
} catch (error) {
|
|
1634
|
+
return {
|
|
1635
|
+
content: [
|
|
1636
|
+
{
|
|
1637
|
+
type: "text",
|
|
1638
|
+
text: formatError(error)
|
|
1639
|
+
}
|
|
1640
|
+
],
|
|
1641
|
+
isError: true
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
async function getAsoLocaleSnapshots(input, client) {
|
|
1646
|
+
try {
|
|
1647
|
+
const response = await client.get(
|
|
1648
|
+
`/api/assets/${input.assetId}/aso/listing-snapshots`,
|
|
1649
|
+
{
|
|
1650
|
+
store: input.store,
|
|
1651
|
+
locale: input.locale,
|
|
1652
|
+
limit: input.limit
|
|
1653
|
+
}
|
|
1654
|
+
);
|
|
1655
|
+
return {
|
|
1656
|
+
content: [
|
|
1657
|
+
{
|
|
1658
|
+
type: "text",
|
|
1659
|
+
text: formatAsJson(response)
|
|
1660
|
+
}
|
|
1661
|
+
]
|
|
1662
|
+
};
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
return {
|
|
1665
|
+
content: [
|
|
1666
|
+
{
|
|
1667
|
+
type: "text",
|
|
1668
|
+
text: formatError(error)
|
|
1669
|
+
}
|
|
1670
|
+
],
|
|
1671
|
+
isError: true
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
async function triggerAsoAnalysis(input, client) {
|
|
1676
|
+
try {
|
|
1677
|
+
const response = await client.post(
|
|
1678
|
+
`/api/assets/${input.assetId}/aso/refetch`,
|
|
1679
|
+
{ force: true }
|
|
1680
|
+
);
|
|
1681
|
+
return {
|
|
1682
|
+
content: [
|
|
1683
|
+
{
|
|
1684
|
+
type: "text",
|
|
1685
|
+
text: formatAsJson(response)
|
|
1686
|
+
}
|
|
1687
|
+
]
|
|
1688
|
+
};
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
return {
|
|
1691
|
+
content: [
|
|
1692
|
+
{
|
|
1693
|
+
type: "text",
|
|
1694
|
+
text: formatError(error)
|
|
1695
|
+
}
|
|
1696
|
+
],
|
|
1697
|
+
isError: true
|
|
1698
|
+
};
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// src/tools/chat.ts
|
|
1703
|
+
import { randomUUID } from "node:crypto";
|
|
1704
|
+
import { z as z12 } from "zod";
|
|
1705
|
+
var listConversationsSchema = z12.object({
|
|
1706
|
+
limit: z12.number().int().min(1).max(100).default(20).describe("Maximum number of conversations to return")
|
|
1707
|
+
});
|
|
1708
|
+
var getConversationMessagesSchema = z12.object({
|
|
1709
|
+
conversationId: z12.string().uuid().describe("The conversation UUID to get messages for")
|
|
1710
|
+
});
|
|
1711
|
+
var sendChatMessageSchema = z12.object({
|
|
1712
|
+
message: z12.string().describe("The message to send to the AI chat"),
|
|
1713
|
+
conversationId: z12.string().uuid().optional().describe("Existing conversation UUID to continue. Starts a new conversation if omitted."),
|
|
1714
|
+
agentType: z12.string().optional().describe("Agent type to use for the conversation (e.g., review, monitoring, forecasting)")
|
|
1715
|
+
});
|
|
1716
|
+
async function listConversations(input, client) {
|
|
1717
|
+
try {
|
|
1718
|
+
const response = await client.get("/api/chat/conversations", {
|
|
1719
|
+
limit: input.limit
|
|
1720
|
+
});
|
|
1721
|
+
const conversations = response.data || response.conversations || response;
|
|
1722
|
+
return {
|
|
1723
|
+
content: [
|
|
1724
|
+
{
|
|
1725
|
+
type: "text",
|
|
1726
|
+
text: formatAsJson({
|
|
1727
|
+
totalConversations: Array.isArray(conversations) ? conversations.length : 0,
|
|
1728
|
+
conversations
|
|
1729
|
+
})
|
|
1730
|
+
}
|
|
1731
|
+
]
|
|
1732
|
+
};
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
return {
|
|
1735
|
+
content: [
|
|
1736
|
+
{
|
|
1737
|
+
type: "text",
|
|
1738
|
+
text: formatError(error)
|
|
1739
|
+
}
|
|
1740
|
+
],
|
|
1741
|
+
isError: true
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
async function getConversationMessages(input, client) {
|
|
1746
|
+
try {
|
|
1747
|
+
const response = await client.get(`/api/chat/messages/${input.conversationId}`);
|
|
1748
|
+
const messages = response.data || response.messages || response;
|
|
1749
|
+
return {
|
|
1750
|
+
content: [
|
|
1751
|
+
{
|
|
1752
|
+
type: "text",
|
|
1753
|
+
text: formatAsJson({
|
|
1754
|
+
conversationId: input.conversationId,
|
|
1755
|
+
totalMessages: Array.isArray(messages) ? messages.length : 0,
|
|
1756
|
+
messages
|
|
1757
|
+
})
|
|
1758
|
+
}
|
|
1759
|
+
]
|
|
1760
|
+
};
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
return {
|
|
1763
|
+
content: [
|
|
1764
|
+
{
|
|
1765
|
+
type: "text",
|
|
1766
|
+
text: formatError(error)
|
|
1767
|
+
}
|
|
1768
|
+
],
|
|
1769
|
+
isError: true
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function parseUiMessageStream(raw) {
|
|
1774
|
+
let reply = "";
|
|
1775
|
+
const toolsUsed = [];
|
|
1776
|
+
for (const line of raw.split("\n")) {
|
|
1777
|
+
if (!line.startsWith("data:"))
|
|
1778
|
+
continue;
|
|
1779
|
+
const payload = line.slice(5).trim();
|
|
1780
|
+
if (!payload || payload === "[DONE]")
|
|
1781
|
+
continue;
|
|
1782
|
+
try {
|
|
1783
|
+
const event = JSON.parse(payload);
|
|
1784
|
+
if (event.type === "text-delta") {
|
|
1785
|
+
reply += String(event.delta ?? event.textDelta ?? "");
|
|
1786
|
+
} else if (typeof event.type === "string" && event.type === "tool-input-start" && typeof event.toolName === "string") {
|
|
1787
|
+
toolsUsed.push(event.toolName);
|
|
1788
|
+
}
|
|
1789
|
+
} catch {
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return { reply, toolsUsed };
|
|
1793
|
+
}
|
|
1794
|
+
async function sendChatMessage(input, client) {
|
|
1795
|
+
try {
|
|
1796
|
+
const conversationId = input.conversationId ?? randomUUID();
|
|
1797
|
+
let priorMessages = [];
|
|
1798
|
+
if (input.conversationId) {
|
|
1799
|
+
const history = await client.get(`/api/chat/messages/${input.conversationId}`, { limit: 1e3 });
|
|
1800
|
+
for (const message of history.data ?? []) {
|
|
1801
|
+
const textParts = (message.parts ?? []).filter(
|
|
1802
|
+
(part) => part.type === "text" && typeof part.text === "string"
|
|
1803
|
+
);
|
|
1804
|
+
if (textParts.length === 0)
|
|
1805
|
+
continue;
|
|
1806
|
+
priorMessages.push({
|
|
1807
|
+
id: message.id,
|
|
1808
|
+
role: message.role,
|
|
1809
|
+
parts: textParts
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
priorMessages = priorMessages.slice(-100);
|
|
1813
|
+
}
|
|
1814
|
+
const body = {
|
|
1815
|
+
id: conversationId,
|
|
1816
|
+
messages: [
|
|
1817
|
+
...priorMessages,
|
|
1818
|
+
{
|
|
1819
|
+
id: randomUUID(),
|
|
1820
|
+
role: "user",
|
|
1821
|
+
parts: [{ type: "text", text: input.message }]
|
|
1822
|
+
}
|
|
1823
|
+
]
|
|
1824
|
+
};
|
|
1825
|
+
if (input.agentType) {
|
|
1826
|
+
body.agentContext = {
|
|
1827
|
+
agentId: null,
|
|
1828
|
+
agentType: input.agentType,
|
|
1829
|
+
agentName: input.agentType,
|
|
1830
|
+
surface: "mcp"
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
const raw = await client.postStream("/api/chat", body);
|
|
1834
|
+
const { reply, toolsUsed } = parseUiMessageStream(raw);
|
|
1835
|
+
return {
|
|
1836
|
+
content: [
|
|
1837
|
+
{
|
|
1838
|
+
type: "text",
|
|
1839
|
+
text: formatAsJson({
|
|
1840
|
+
conversationId,
|
|
1841
|
+
reply: reply || "The assistant returned no text response.",
|
|
1842
|
+
toolsUsed,
|
|
1843
|
+
tip: "Pass this conversationId to continue the conversation or to get_conversation_messages for the full transcript."
|
|
1844
|
+
})
|
|
1845
|
+
}
|
|
1846
|
+
],
|
|
1847
|
+
...reply ? {} : { isError: true }
|
|
1848
|
+
};
|
|
1849
|
+
} catch (error) {
|
|
1850
|
+
return {
|
|
1851
|
+
content: [
|
|
1852
|
+
{
|
|
1853
|
+
type: "text",
|
|
1854
|
+
text: formatError(error)
|
|
1855
|
+
}
|
|
1856
|
+
],
|
|
1857
|
+
isError: true
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// src/tools/index.ts
|
|
1863
|
+
function registerTools(server, client, options = {}) {
|
|
1864
|
+
const rateLimiter = options.rateLimiter ?? new RateLimiter(100, 6e4);
|
|
1865
|
+
function wrapTool(handler) {
|
|
1866
|
+
const callback = async (input) => {
|
|
1867
|
+
const rateCheck = rateLimiter.check("default");
|
|
1868
|
+
if (!rateCheck.allowed) {
|
|
1869
|
+
return {
|
|
1870
|
+
content: [
|
|
1871
|
+
{
|
|
1872
|
+
type: "text",
|
|
1873
|
+
text: `Rate limit exceeded. Try again in ${Math.ceil(
|
|
1874
|
+
rateCheck.retryAfterMs / 1e3
|
|
1875
|
+
)}s. Limit: 100 requests/minute.`
|
|
1876
|
+
}
|
|
1877
|
+
],
|
|
1878
|
+
isError: true
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
return handler(input, client);
|
|
1882
|
+
};
|
|
1883
|
+
return callback;
|
|
1884
|
+
}
|
|
1885
|
+
function tool(name, title, description, schema, handler, annotations) {
|
|
1886
|
+
server.registerTool(
|
|
1887
|
+
name,
|
|
1888
|
+
{
|
|
1889
|
+
title,
|
|
1890
|
+
description,
|
|
1891
|
+
inputSchema: schema.shape,
|
|
1892
|
+
annotations: { title, ...annotations }
|
|
1893
|
+
},
|
|
1894
|
+
wrapTool(handler)
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
tool(
|
|
1898
|
+
"list_apps",
|
|
1899
|
+
"List apps",
|
|
1900
|
+
"List all mobile apps in your Fload organization. Returns app metadata including name, bundle ID, platform (iOS/Android), icon URL, and category.",
|
|
1901
|
+
listAppsSchema,
|
|
1902
|
+
listApps,
|
|
1903
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1904
|
+
);
|
|
1905
|
+
tool(
|
|
1906
|
+
"get_app_details",
|
|
1907
|
+
"Get app details",
|
|
1908
|
+
"Get detailed information about a specific app, including metadata, connected data sources (App Store Connect, Google Play Console), and sync status. Provide either assetId (UUID) or bundleId.",
|
|
1909
|
+
getAppDetailsSchema,
|
|
1910
|
+
getAppDetails,
|
|
1911
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1912
|
+
);
|
|
1913
|
+
tool(
|
|
1914
|
+
"get_reviews",
|
|
1915
|
+
"Get reviews",
|
|
1916
|
+
"Get app reviews with flexible filtering. Filter by app (assetId or bundleId), platform, rating (1-5 stars), replied status, and date range. Returns reviews with metadata, author, body text, and reply status. Useful for sentiment analysis, support workflows, and review management.",
|
|
1917
|
+
getReviewsSchema,
|
|
1918
|
+
getReviews,
|
|
1919
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1920
|
+
);
|
|
1921
|
+
tool(
|
|
1922
|
+
"generate_review_reply",
|
|
1923
|
+
"Generate review reply (AI draft)",
|
|
1924
|
+
"Generate an AI draft reply for an app review. The AI uses the review context and any configured agent settings (tone, custom instructions) to craft a response. Returns the generated draft text. Does not publish \u2014 call send_review_reply or approve_action to publish the draft.",
|
|
1925
|
+
generateReviewReplySchema,
|
|
1926
|
+
generateReviewReply,
|
|
1927
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
1928
|
+
);
|
|
1929
|
+
tool(
|
|
1930
|
+
"send_review_reply",
|
|
1931
|
+
"Send review reply",
|
|
1932
|
+
"Send a reply to an app review on the App Store or Google Play. The response text will be submitted as the developer response. This is a write operation that publishes the reply publicly \u2014 treat as destructive (cannot be silently undone).",
|
|
1933
|
+
sendReviewReplySchema,
|
|
1934
|
+
sendReviewReply,
|
|
1935
|
+
{ readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
1936
|
+
);
|
|
1937
|
+
tool(
|
|
1938
|
+
"translate_review",
|
|
1939
|
+
"Translate review",
|
|
1940
|
+
"Translate a review to English. Useful for reviews written in other languages. Pass the review content fields (rating, title, body, storeFront, appVersionString) from a get_reviews result. Returns the translated text. Does not modify the review in Fload.",
|
|
1941
|
+
translateReviewSchema,
|
|
1942
|
+
translateReview,
|
|
1943
|
+
{ readOnlyHint: true, openWorldHint: true }
|
|
1087
1944
|
);
|
|
1088
|
-
|
|
1945
|
+
tool(
|
|
1946
|
+
"discover_metrics",
|
|
1947
|
+
"Discover available metrics",
|
|
1948
|
+
"Discover what metrics are available for an app. Returns all available metrics organized by category (revenue, downloads, subscriptions, engagement, ads). Always call this first before querying metrics to know what data exists.",
|
|
1949
|
+
discoverMetricsSchema,
|
|
1950
|
+
discoverMetrics,
|
|
1951
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1952
|
+
);
|
|
1953
|
+
tool(
|
|
1089
1954
|
"get_metrics",
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1955
|
+
"Get metrics",
|
|
1956
|
+
"Query metric timeseries data for an app. Supports 30+ metrics (proceeds, totalDownloads, activeSubs, sessions, crashes, adSpend, etc.). Can query multiple metrics at once. Supports dimensional breakdowns (by country, platform, campaign). Use discover_metrics first to see available metrics.",
|
|
1957
|
+
getMetricsSchema,
|
|
1958
|
+
getMetrics,
|
|
1959
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1095
1960
|
);
|
|
1096
|
-
|
|
1961
|
+
tool(
|
|
1097
1962
|
"discover_dimensions",
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1963
|
+
"Discover available dimensions",
|
|
1964
|
+
"Discover available dimensions for breaking down metrics (e.g., country, platform, app version, campaign). Optionally get the available values for a specific dimension.",
|
|
1965
|
+
discoverDimensionsSchema,
|
|
1966
|
+
discoverDimensions,
|
|
1967
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1103
1968
|
);
|
|
1104
|
-
|
|
1969
|
+
tool(
|
|
1105
1970
|
"list_agents",
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1971
|
+
"List agents",
|
|
1972
|
+
"List all available AI agents in the Fload platform with their current status. Returns agent types (review, monitoring, forecasting, growth, aso, ads, product, submission_review) and configuration status.",
|
|
1973
|
+
listAgentsSchema,
|
|
1974
|
+
listAgents,
|
|
1975
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1111
1976
|
);
|
|
1112
|
-
|
|
1977
|
+
tool(
|
|
1113
1978
|
"get_agent_details",
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1979
|
+
"Get agent details",
|
|
1980
|
+
"Get detailed configuration and status for a specific agent type. For the review agent, returns per-asset settings (mode, tone, custom instructions). For the product agent, returns latest run details.",
|
|
1981
|
+
getAgentDetailsSchema,
|
|
1982
|
+
getAgentDetails,
|
|
1983
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1119
1984
|
);
|
|
1120
|
-
|
|
1985
|
+
tool(
|
|
1121
1986
|
"get_agent_run_history",
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1987
|
+
"Get agent run history",
|
|
1988
|
+
"Get run history for a specific agent type. Currently available for the product agent (BrowserStack app installation runs). Returns run status, timing, and error details.",
|
|
1989
|
+
getAgentRunHistorySchema,
|
|
1990
|
+
getAgentRunHistory,
|
|
1991
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1992
|
+
);
|
|
1993
|
+
tool(
|
|
1994
|
+
"trigger_agent_run",
|
|
1995
|
+
"Trigger agent run",
|
|
1996
|
+
"Trigger a manual run for an agent. Optionally specify an asset (app) to run the agent against. Returns the triggered run details. Reversible in the sense that agent runs can be paused or dismissed.",
|
|
1997
|
+
triggerAgentRunSchema,
|
|
1998
|
+
triggerAgentRun,
|
|
1999
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
2000
|
+
);
|
|
2001
|
+
tool(
|
|
2002
|
+
"pause_agent",
|
|
2003
|
+
"Pause agent",
|
|
2004
|
+
"Pause a running agent. The agent will stop processing until resumed. Reversible via resume_agent.",
|
|
2005
|
+
pauseAgentSchema,
|
|
2006
|
+
pauseAgent,
|
|
2007
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
2008
|
+
);
|
|
2009
|
+
tool(
|
|
2010
|
+
"resume_agent",
|
|
2011
|
+
"Resume agent",
|
|
2012
|
+
"Resume a paused agent. The agent will continue processing from where it left off. Reversible via pause_agent.",
|
|
2013
|
+
resumeAgentSchema,
|
|
2014
|
+
resumeAgent,
|
|
2015
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1127
2016
|
);
|
|
1128
|
-
|
|
2017
|
+
tool(
|
|
2018
|
+
"get_agent_activity",
|
|
2019
|
+
"Get agent activity",
|
|
2020
|
+
"Get the recent activity log for an agent. Returns a chronological list of actions the agent has taken, including timestamps, event types, and details.",
|
|
2021
|
+
getAgentActivitySchema,
|
|
2022
|
+
getAgentActivity,
|
|
2023
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
2024
|
+
);
|
|
2025
|
+
tool(
|
|
1129
2026
|
"get_anomalies",
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
2027
|
+
"Get anomalies",
|
|
2028
|
+
"Get detected anomalies (unusual metric changes) for your apps. Filter by app, severity (low/medium/high/critical), type (surge/decline), status, metric name, and date range. Returns actual vs expected values, deviation percentage, confidence, and suggested actions.",
|
|
2029
|
+
getAnomaliesSchema,
|
|
2030
|
+
getAnomalies,
|
|
2031
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
2032
|
+
);
|
|
2033
|
+
tool(
|
|
2034
|
+
"get_anomaly_detail",
|
|
2035
|
+
"Get anomaly detail",
|
|
2036
|
+
"Get full detail for a single anomaly including chart data. Returns the anomaly metadata, actual vs expected values, and historical metric data points for visualization.",
|
|
2037
|
+
getAnomalyDetailSchema,
|
|
2038
|
+
getAnomalyDetail,
|
|
2039
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
2040
|
+
);
|
|
2041
|
+
tool(
|
|
2042
|
+
"acknowledge_anomaly",
|
|
2043
|
+
"Acknowledge anomaly",
|
|
2044
|
+
'Mark an anomaly as acknowledged. This updates the anomaly status from "new" to "acknowledged", indicating it has been reviewed but not dismissed. Reversible \u2014 anomaly can be reset or dismissed later.',
|
|
2045
|
+
acknowledgeAnomalySchema,
|
|
2046
|
+
acknowledgeAnomaly,
|
|
2047
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
1135
2048
|
);
|
|
1136
|
-
|
|
2049
|
+
tool(
|
|
2050
|
+
"dismiss_anomaly",
|
|
2051
|
+
"Dismiss anomaly",
|
|
2052
|
+
'Dismiss an anomaly. This updates the anomaly status to "dismissed", removing it from active alerts. Dismissed anomalies are excluded from queries by default. Soft state change \u2014 the anomaly row is preserved and can be un-dismissed later.',
|
|
2053
|
+
dismissAnomalySchema,
|
|
2054
|
+
dismissAnomaly,
|
|
2055
|
+
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
|
|
2056
|
+
);
|
|
2057
|
+
tool(
|
|
1137
2058
|
"get_ads_performance",
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
2059
|
+
"Get ads performance",
|
|
2060
|
+
"Get ad campaign performance data across platforms (Apple Search Ads, Google Ads, Meta Ads, TikTok Ads). Returns campaign metadata, status, and for Apple Search Ads includes daily performance snapshots (spend, impressions, taps, installs, CPI, TTR, conversion rate).",
|
|
2061
|
+
getAdsPerformanceSchema,
|
|
2062
|
+
getAdsPerformance,
|
|
2063
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1143
2064
|
);
|
|
1144
|
-
|
|
2065
|
+
tool(
|
|
1145
2066
|
"get_growth_audit",
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
2067
|
+
"Get growth audit",
|
|
2068
|
+
"Get a comprehensive growth audit for an app. Synthesizes data from review sentiment analysis, recent anomalies, valuation trends, and connector health into an actionable growth assessment.",
|
|
2069
|
+
getGrowthAuditSchema,
|
|
2070
|
+
getGrowthAudit,
|
|
2071
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1151
2072
|
);
|
|
1152
|
-
|
|
2073
|
+
tool(
|
|
1153
2074
|
"get_growth_score",
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
2075
|
+
"Get growth score",
|
|
2076
|
+
"Get a calculated growth score (0-100) and grade (A-F) for an app. The score is based on app store rating, valuation trend, recent anomalies, review sentiment, and data connector health. Includes a breakdown of scoring factors.",
|
|
2077
|
+
getGrowthScoreSchema,
|
|
2078
|
+
getGrowthScore,
|
|
2079
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1159
2080
|
);
|
|
1160
|
-
|
|
2081
|
+
tool(
|
|
1161
2082
|
"get_forecasts",
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
2083
|
+
"Get forecasts",
|
|
2084
|
+
"Get a statistical forecast for an app metric (proceeds, total_revenue, total_downloads, active_subs, or new_trials). Returns weekly historical values plus a forward projection with confidence bounds.",
|
|
2085
|
+
getForecastsSchema,
|
|
2086
|
+
getForecasts,
|
|
2087
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1167
2088
|
);
|
|
1168
|
-
|
|
2089
|
+
tool(
|
|
1169
2090
|
"get_dashboard_overview",
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
2091
|
+
"Get dashboard overview",
|
|
2092
|
+
"Get an aggregated dashboard overview for the organization. Returns portfolio summary (apps, valuations, ratings), data connector health status, and alerts (recent anomalies, pending review drafts).",
|
|
2093
|
+
getDashboardOverviewSchema,
|
|
2094
|
+
getDashboardOverview,
|
|
2095
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1175
2096
|
);
|
|
1176
|
-
|
|
2097
|
+
tool(
|
|
1177
2098
|
"list_pending_actions",
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
2099
|
+
"List pending actions",
|
|
2100
|
+
"List pending actions awaiting approval. Currently shows AI-generated review draft replies that have not been sent yet. Includes the original review context and the drafted reply. Filter by app.",
|
|
2101
|
+
listPendingActionsSchema,
|
|
2102
|
+
listPendingActions,
|
|
2103
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1183
2104
|
);
|
|
1184
|
-
|
|
2105
|
+
tool(
|
|
1185
2106
|
"approve_action",
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
2107
|
+
"Approve pending action",
|
|
2108
|
+
"Approve a pending action (e.g., a review draft reply). Approving a review reply publishes it to the store \u2014 treat as destructive (publicly visible, cannot be silently undone). Optionally edit the reply text before approving.",
|
|
2109
|
+
approveActionSchema,
|
|
2110
|
+
approveAction,
|
|
2111
|
+
{ readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
1191
2112
|
);
|
|
1192
|
-
|
|
2113
|
+
tool(
|
|
1193
2114
|
"reject_action",
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
2115
|
+
"Reject pending action",
|
|
2116
|
+
"Reject a pending action (e.g., delete a review draft reply). The draft will be permanently removed. Destructive \u2014 not recoverable without regenerating the draft.",
|
|
2117
|
+
rejectActionSchema,
|
|
2118
|
+
rejectAction,
|
|
2119
|
+
{ readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
2120
|
+
);
|
|
2121
|
+
tool(
|
|
2122
|
+
"get_aso_summary",
|
|
2123
|
+
"Get ASO summary",
|
|
2124
|
+
"Get the ASO (App Store Optimization) score, health status, and overview for an app. Returns an overall optimization score and key ASO health indicators. Use this for a quick snapshot of how well an app is optimized for store search and discovery.",
|
|
2125
|
+
getAsoSummarySchema,
|
|
2126
|
+
getAsoSummary,
|
|
2127
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
1199
2128
|
);
|
|
1200
|
-
|
|
1201
|
-
|
|
2129
|
+
tool(
|
|
2130
|
+
"get_aso_recommendations",
|
|
2131
|
+
"Get ASO recommendations",
|
|
2132
|
+
"Get actionable ASO recommendations for an app, including suggested improvements to the title, subtitle, keywords, and description. Each recommendation explains the rationale and expected impact on search visibility.",
|
|
2133
|
+
getAsoRecommendationsSchema,
|
|
2134
|
+
getAsoRecommendations,
|
|
2135
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
2136
|
+
);
|
|
2137
|
+
tool(
|
|
2138
|
+
"get_aso_keywords",
|
|
2139
|
+
"Get ASO keywords",
|
|
2140
|
+
'Get keyword intelligence for an app \u2014 current keyword rankings, search volume estimates, difficulty scores, and competitor keyword data. Optionally filter by locale (e.g., "en-US"). Useful for identifying keyword opportunities and tracking ranking changes.',
|
|
2141
|
+
getAsoKeywordsSchema,
|
|
2142
|
+
getAsoKeywords,
|
|
2143
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
2144
|
+
);
|
|
2145
|
+
tool(
|
|
2146
|
+
"get_aso_experiments",
|
|
2147
|
+
"Get ASO experiments",
|
|
2148
|
+
"List ASO experiments (A/B tests and metadata changes) for an app. Filter by status: proposed, approved, applied, measuring, completed, or reverted. Returns experiment details, variants, and results when available. Supports pagination.",
|
|
2149
|
+
getAsoExperimentsSchema,
|
|
2150
|
+
getAsoExperiments,
|
|
2151
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
2152
|
+
);
|
|
2153
|
+
tool(
|
|
2154
|
+
"get_aso_locale_snapshots",
|
|
2155
|
+
"Get ASO locale snapshots",
|
|
2156
|
+
"Get current App Store and Google Play listing snapshots across all locales for an app. Returns the live title, subtitle, keywords, description, and promotional text for each locale. Useful for auditing localized metadata consistency.",
|
|
2157
|
+
getAsoLocaleSnapshotsSchema,
|
|
2158
|
+
getAsoLocaleSnapshots,
|
|
2159
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
2160
|
+
);
|
|
2161
|
+
tool(
|
|
2162
|
+
"trigger_aso_analysis",
|
|
2163
|
+
"Trigger ASO analysis",
|
|
2164
|
+
"Trigger a new ASO analysis run for an app. This kicks off a fresh evaluation of the app's store listing metadata, keyword rankings, and competitive positioning. Results will be reflected in subsequent calls to get_aso_summary and get_aso_recommendations.",
|
|
2165
|
+
triggerAsoAnalysisSchema,
|
|
2166
|
+
triggerAsoAnalysis,
|
|
2167
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
2168
|
+
);
|
|
2169
|
+
tool(
|
|
2170
|
+
"list_conversations",
|
|
2171
|
+
"List conversations",
|
|
2172
|
+
"List chat conversations in the Fload AI chat. Returns conversation metadata including title, creation date, and last message preview. Use limit to control how many are returned.",
|
|
2173
|
+
listConversationsSchema,
|
|
2174
|
+
listConversations,
|
|
2175
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
2176
|
+
);
|
|
2177
|
+
tool(
|
|
2178
|
+
"get_conversation_messages",
|
|
2179
|
+
"Get conversation messages",
|
|
2180
|
+
"Get all messages in a specific chat conversation. Returns the full message history including user messages and AI responses with timestamps and roles.",
|
|
2181
|
+
getConversationMessagesSchema,
|
|
2182
|
+
getConversationMessages,
|
|
2183
|
+
{ readOnlyHint: true, openWorldHint: false }
|
|
2184
|
+
);
|
|
2185
|
+
tool(
|
|
2186
|
+
"send_chat_message",
|
|
2187
|
+
"Send chat message",
|
|
2188
|
+
"Send a message to the Fload AI chat assistant. Starts a new conversation if no conversationId is provided, or continues an existing one. Optionally specify an agentType to route to a specialized agent (review, monitoring, forecasting, etc.). Adds a message to your chat history \u2014 not publicly visible.",
|
|
2189
|
+
sendChatMessageSchema,
|
|
2190
|
+
sendChatMessage,
|
|
2191
|
+
{ readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/server.ts
|
|
2196
|
+
async function createMcpServer(_config, client) {
|
|
2197
|
+
console.error("[Fload MCP] Server initializing...");
|
|
2198
|
+
const server = new McpServer({
|
|
2199
|
+
name: "fload",
|
|
2200
|
+
version: "0.1.0"
|
|
2201
|
+
});
|
|
2202
|
+
registerTools(server, client);
|
|
2203
|
+
console.error("[Fload MCP] Registered tools");
|
|
1202
2204
|
return server;
|
|
1203
2205
|
}
|
|
1204
2206
|
|
|
1205
|
-
// src/
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
headers: {
|
|
1216
|
-
"Authorization": `Bearer ${this.apiKey}`,
|
|
1217
|
-
"Content-Type": "application/json",
|
|
1218
|
-
...options.headers
|
|
1219
|
-
}
|
|
1220
|
-
});
|
|
1221
|
-
if (!response.ok) {
|
|
1222
|
-
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
1223
|
-
throw new Error(error.message || `API error: ${response.status}`);
|
|
1224
|
-
}
|
|
1225
|
-
return response.json();
|
|
1226
|
-
}
|
|
1227
|
-
async get(path, params) {
|
|
1228
|
-
const searchParams = new URLSearchParams();
|
|
1229
|
-
if (params) {
|
|
1230
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1231
|
-
if (value !== void 0)
|
|
1232
|
-
searchParams.set(key, String(value));
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
const query = searchParams.toString();
|
|
1236
|
-
return this.request(`${path}${query ? `?${query}` : ""}`);
|
|
1237
|
-
}
|
|
1238
|
-
async post(path, body) {
|
|
1239
|
-
return this.request(path, {
|
|
1240
|
-
method: "POST",
|
|
1241
|
-
body: body ? JSON.stringify(body) : void 0
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
async patch(path, body) {
|
|
1245
|
-
return this.request(path, {
|
|
1246
|
-
method: "PATCH",
|
|
1247
|
-
body: body ? JSON.stringify(body) : void 0
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
async delete(path) {
|
|
1251
|
-
return this.request(path, { method: "DELETE" });
|
|
2207
|
+
// src/config.ts
|
|
2208
|
+
import "dotenv/config";
|
|
2209
|
+
import { readFileSync, existsSync } from "fs";
|
|
2210
|
+
import { homedir } from "os";
|
|
2211
|
+
import { join } from "path";
|
|
2212
|
+
function loadConfig() {
|
|
2213
|
+
const apiKey = process.env.FLOAD_API_KEY;
|
|
2214
|
+
const apiUrl = process.env.FLOAD_API_URL || "https://api.fload.com";
|
|
2215
|
+
if (apiKey) {
|
|
2216
|
+
return { apiKey, apiUrl };
|
|
1252
2217
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
// src/index.ts
|
|
1256
|
-
async function main() {
|
|
1257
|
-
try {
|
|
1258
|
-
console.error("[Fload MCP] Starting server...");
|
|
1259
|
-
const config = loadConfig();
|
|
1260
|
-
console.error("[Fload MCP] Configuration loaded");
|
|
1261
|
-
console.error(`[Fload MCP] API URL: ${config.apiUrl}`);
|
|
1262
|
-
const client = new FloadApiClient(config.apiUrl, config.apiKey);
|
|
2218
|
+
const configPath = join(homedir(), ".fload", "config.json");
|
|
2219
|
+
if (existsSync(configPath)) {
|
|
1263
2220
|
try {
|
|
1264
|
-
|
|
1265
|
-
|
|
2221
|
+
const configData = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
2222
|
+
if (configData.apiKey) {
|
|
2223
|
+
return {
|
|
2224
|
+
apiKey: configData.apiKey,
|
|
2225
|
+
apiUrl: configData.apiUrl || apiUrl
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
1266
2228
|
} catch (error) {
|
|
1267
|
-
console.error("
|
|
1268
|
-
console.error("[Fload MCP] Continuing anyway \u2014 API may become available");
|
|
2229
|
+
console.error("Failed to parse config file:", error);
|
|
1269
2230
|
}
|
|
1270
|
-
const server = await createMcpServer(config, client);
|
|
1271
|
-
const transport = new StdioServerTransport();
|
|
1272
|
-
await server.connect(transport);
|
|
1273
|
-
console.error("[Fload MCP] Server running on stdio");
|
|
1274
|
-
} catch (error) {
|
|
1275
|
-
console.error("[Fload MCP] Fatal error:", error);
|
|
1276
|
-
process.exit(1);
|
|
1277
2231
|
}
|
|
2232
|
+
throw new Error(
|
|
2233
|
+
'Fload MCP requires an API key. Set FLOAD_API_KEY environment variable or create ~/.fload/config.json with {"apiKey": "fload_sk_..."}'
|
|
2234
|
+
);
|
|
1278
2235
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
});
|
|
1287
|
-
main().catch((error) => {
|
|
1288
|
-
console.error("[Fload MCP] Unhandled error:", error);
|
|
1289
|
-
process.exit(1);
|
|
1290
|
-
});
|
|
2236
|
+
export {
|
|
2237
|
+
FloadApiClient,
|
|
2238
|
+
RateLimiter,
|
|
2239
|
+
createMcpServer,
|
|
2240
|
+
loadConfig,
|
|
2241
|
+
registerTools
|
|
2242
|
+
};
|
|
1291
2243
|
//# sourceMappingURL=index.js.map
|