@fload-ai/mcp 0.2.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 +1 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/api-client.d.ts +5 -4
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +59 -6
- package/dist/api-client.js.map +3 -3
- package/dist/bin.js +450 -184
- package/dist/bin.js.map +4 -4
- package/dist/index.js +450 -184
- package/dist/index.js.map +4 -4
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/public-errors.d.ts +2 -0
- package/dist/public-errors.d.ts.map +1 -0
- package/dist/tools/actions.d.ts +0 -3
- package/dist/tools/actions.d.ts.map +1 -1
- package/dist/tools/ads.d.ts +2 -2
- package/dist/tools/ads.d.ts.map +1 -1
- package/dist/tools/agents.d.ts +4 -4
- package/dist/tools/agents.d.ts.map +1 -1
- package/dist/tools/analytics.d.ts +2 -2
- package/dist/tools/analytics.d.ts.map +1 -1
- package/dist/tools/anomalies.d.ts +2 -2
- package/dist/tools/anomalies.d.ts.map +1 -1
- package/dist/tools/apps.d.ts +2 -2
- package/dist/tools/apps.d.ts.map +1 -1
- package/dist/tools/aso.d.ts +14 -5
- package/dist/tools/aso.d.ts.map +1 -1
- package/dist/tools/chat.d.ts +7 -9
- package/dist/tools/chat.d.ts.map +1 -1
- package/dist/tools/dashboard.d.ts.map +1 -1
- package/dist/tools/forecasting.d.ts +9 -3
- package/dist/tools/forecasting.d.ts.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +392 -178
- package/dist/tools/index.js.map +3 -3
- package/dist/tools/reviews.d.ts +19 -4
- package/dist/tools/reviews.d.ts.map +1 -1
- package/package.json +11 -13
package/dist/bin.js
CHANGED
|
@@ -91,9 +91,12 @@ async function listApps(input, client) {
|
|
|
91
91
|
try {
|
|
92
92
|
const response = await client.get("/api/assets", {
|
|
93
93
|
limit: input.limit,
|
|
94
|
-
offset: 0
|
|
94
|
+
offset: 0,
|
|
95
|
+
// The listing hides assets without a valuation by default; "list all
|
|
96
|
+
// apps" must include apps that haven't been valued yet (FLO-734).
|
|
97
|
+
includeUnvalued: "true"
|
|
95
98
|
});
|
|
96
|
-
const apps = response.data
|
|
99
|
+
const apps = response.data ?? [];
|
|
97
100
|
const filtered = input.platform ? apps.filter((app) => {
|
|
98
101
|
if (input.platform === "ios")
|
|
99
102
|
return !!app.appleAppId;
|
|
@@ -137,8 +140,14 @@ async function getAppDetails(input, client) {
|
|
|
137
140
|
}
|
|
138
141
|
let assetId = input.assetId;
|
|
139
142
|
if (!assetId && input.bundleId) {
|
|
140
|
-
const allApps = await client.get("/api/assets", {
|
|
141
|
-
|
|
143
|
+
const allApps = await client.get("/api/assets", {
|
|
144
|
+
limit: 100,
|
|
145
|
+
offset: 0,
|
|
146
|
+
includeUnvalued: "true"
|
|
147
|
+
});
|
|
148
|
+
const match = (allApps.data ?? []).find(
|
|
149
|
+
(app2) => app2.bundleId === input.bundleId
|
|
150
|
+
);
|
|
142
151
|
if (!match) {
|
|
143
152
|
return {
|
|
144
153
|
content: [
|
|
@@ -152,8 +161,13 @@ async function getAppDetails(input, client) {
|
|
|
152
161
|
}
|
|
153
162
|
assetId = match.id;
|
|
154
163
|
}
|
|
155
|
-
const response = await client.get(
|
|
156
|
-
|
|
164
|
+
const response = await client.get(
|
|
165
|
+
`/api/assets/${assetId}`
|
|
166
|
+
);
|
|
167
|
+
const app = response.data;
|
|
168
|
+
if (!app) {
|
|
169
|
+
throw new Error("App details response did not include app data");
|
|
170
|
+
}
|
|
157
171
|
return {
|
|
158
172
|
content: [
|
|
159
173
|
{
|
|
@@ -210,14 +224,27 @@ var sendReviewReplySchema = z2.object({
|
|
|
210
224
|
});
|
|
211
225
|
var translateReviewSchema = z2.object({
|
|
212
226
|
reviewId: z2.string().describe("The review UUID to translate"),
|
|
213
|
-
assetId: z2.string().uuid().describe("The app UUID the review belongs to")
|
|
227
|
+
assetId: z2.string().uuid().describe("The app UUID the review belongs to"),
|
|
228
|
+
// The translate endpoint receives the review content in the request body
|
|
229
|
+
// (it does not re-fetch it). Pass the fields from a get_reviews result.
|
|
230
|
+
rating: z2.number().int().min(1).max(5).describe("The review star rating, from get_reviews"),
|
|
231
|
+
title: z2.string().default("").describe("The review title, from get_reviews"),
|
|
232
|
+
body: z2.string().describe("The review text to translate, from get_reviews"),
|
|
233
|
+
storeFront: z2.string().default("").describe('The review storefront (e.g. "SE"), from get_reviews'),
|
|
234
|
+
appVersionString: z2.string().default("").describe("The app version the review was left on, from get_reviews")
|
|
214
235
|
});
|
|
215
236
|
async function getReviews(input, client) {
|
|
216
237
|
try {
|
|
217
238
|
let assetId = input.assetId;
|
|
218
239
|
if (!assetId && input.bundleId) {
|
|
219
|
-
const allApps = await client.get("/api/assets", {
|
|
220
|
-
|
|
240
|
+
const allApps = await client.get("/api/assets", {
|
|
241
|
+
limit: 100,
|
|
242
|
+
offset: 0,
|
|
243
|
+
includeUnvalued: "true"
|
|
244
|
+
});
|
|
245
|
+
const match = (allApps.data ?? []).find(
|
|
246
|
+
(app) => app.bundleId === input.bundleId
|
|
247
|
+
);
|
|
221
248
|
if (!match) {
|
|
222
249
|
return {
|
|
223
250
|
content: [
|
|
@@ -231,12 +258,20 @@ async function getReviews(input, client) {
|
|
|
231
258
|
}
|
|
232
259
|
assetId = match.id;
|
|
233
260
|
}
|
|
261
|
+
if (!assetId) {
|
|
262
|
+
return {
|
|
263
|
+
content: [
|
|
264
|
+
{
|
|
265
|
+
type: "text",
|
|
266
|
+
text: "Provide an assetId (or bundleId) to fetch reviews for. Use list_apps to find app IDs."
|
|
267
|
+
}
|
|
268
|
+
],
|
|
269
|
+
isError: true
|
|
270
|
+
};
|
|
271
|
+
}
|
|
234
272
|
const params = {
|
|
235
|
-
limit: input.limit
|
|
236
|
-
sortBy: input.sortBy
|
|
273
|
+
limit: input.limit
|
|
237
274
|
};
|
|
238
|
-
if (assetId)
|
|
239
|
-
params.assetId = assetId;
|
|
240
275
|
if (input.platform)
|
|
241
276
|
params.platform = input.platform;
|
|
242
277
|
if (input.rating !== void 0)
|
|
@@ -247,13 +282,25 @@ async function getReviews(input, client) {
|
|
|
247
282
|
params.startDate = input.startDate;
|
|
248
283
|
if (input.endDate)
|
|
249
284
|
params.endDate = input.endDate;
|
|
250
|
-
const response = await client.get(
|
|
251
|
-
|
|
285
|
+
const response = await client.get(
|
|
286
|
+
`/api/assets/${assetId}/reviews`,
|
|
287
|
+
params
|
|
288
|
+
);
|
|
289
|
+
let reviews = response.data?.reviews ?? [];
|
|
290
|
+
if (input.sortBy === "rating") {
|
|
291
|
+
reviews = [...reviews].sort(
|
|
292
|
+
(a, b) => Number(b.rating ?? 0) - Number(a.rating ?? 0)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const hasReply = (review) => !!(review.developerResponse || review.replySource || review.hasReply);
|
|
252
296
|
const summary = {
|
|
253
297
|
totalReviews: reviews.length,
|
|
254
|
-
averageRating: reviews.length > 0 ? (reviews.reduce((sum,
|
|
255
|
-
|
|
256
|
-
|
|
298
|
+
averageRating: reviews.length > 0 ? (reviews.reduce((sum, review) => {
|
|
299
|
+
const rating = Number(review.rating ?? 0);
|
|
300
|
+
return sum + (Number.isFinite(rating) ? rating : 0);
|
|
301
|
+
}, 0) / reviews.length).toFixed(2) : 0,
|
|
302
|
+
repliedCount: reviews.filter((review) => hasReply(review)).length,
|
|
303
|
+
unrepliedCount: reviews.filter((review) => !hasReply(review)).length
|
|
257
304
|
};
|
|
258
305
|
return {
|
|
259
306
|
content: [
|
|
@@ -261,19 +308,23 @@ async function getReviews(input, client) {
|
|
|
261
308
|
type: "text",
|
|
262
309
|
text: formatAsJson({
|
|
263
310
|
summary,
|
|
264
|
-
reviews: reviews.map((
|
|
265
|
-
id:
|
|
266
|
-
appId:
|
|
267
|
-
platform:
|
|
268
|
-
rating:
|
|
269
|
-
title:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
311
|
+
reviews: reviews.map((review) => ({
|
|
312
|
+
id: review.id,
|
|
313
|
+
appId: review.appId,
|
|
314
|
+
platform: review.platform,
|
|
315
|
+
rating: review.rating,
|
|
316
|
+
title: review.title,
|
|
317
|
+
// The handler exposes the review text as `review`.
|
|
318
|
+
body: review.review ?? review.body,
|
|
319
|
+
author: review.nickname || review.author,
|
|
320
|
+
// Epoch ms from the handler.
|
|
321
|
+
date: review.lastModified || review.date,
|
|
322
|
+
version: review.appVersionString || review.version,
|
|
323
|
+
storeFront: review.storeFront,
|
|
324
|
+
hasReply: hasReply(review),
|
|
325
|
+
replySource: review.replySource ?? null,
|
|
326
|
+
hasDraft: review.hasDraft ?? false,
|
|
327
|
+
reply: review.developerResponse ? typeof review.developerResponse === "object" ? review.developerResponse.response : review.developerResponse : review.reply || null
|
|
277
328
|
}))
|
|
278
329
|
})
|
|
279
330
|
}
|
|
@@ -293,9 +344,10 @@ async function getReviews(input, client) {
|
|
|
293
344
|
}
|
|
294
345
|
async function generateReviewReply(input, client) {
|
|
295
346
|
try {
|
|
296
|
-
const response = await client.post(
|
|
297
|
-
assetId
|
|
298
|
-
|
|
347
|
+
const response = await client.post(
|
|
348
|
+
`/api/assets/${input.assetId}/reviews/${input.reviewId}/generate-reply`,
|
|
349
|
+
void 0
|
|
350
|
+
);
|
|
299
351
|
return {
|
|
300
352
|
content: [
|
|
301
353
|
{
|
|
@@ -318,10 +370,11 @@ async function generateReviewReply(input, client) {
|
|
|
318
370
|
}
|
|
319
371
|
async function sendReviewReply(input, client) {
|
|
320
372
|
try {
|
|
321
|
-
const response = await client.post(
|
|
322
|
-
assetId
|
|
323
|
-
|
|
324
|
-
|
|
373
|
+
const response = await client.post(
|
|
374
|
+
`/api/assets/${input.assetId}/reviews/${input.reviewId}/respond`,
|
|
375
|
+
// The API handler reads `responseText` from the body.
|
|
376
|
+
{ responseText: input.response }
|
|
377
|
+
);
|
|
325
378
|
return {
|
|
326
379
|
content: [
|
|
327
380
|
{
|
|
@@ -344,9 +397,18 @@ async function sendReviewReply(input, client) {
|
|
|
344
397
|
}
|
|
345
398
|
async function translateReview(input, client) {
|
|
346
399
|
try {
|
|
347
|
-
const response = await client.post(
|
|
348
|
-
assetId
|
|
349
|
-
|
|
400
|
+
const response = await client.post(
|
|
401
|
+
`/api/assets/${input.assetId}/reviews/${input.reviewId}/translate`,
|
|
402
|
+
// The translate endpoint expects the review content in the body.
|
|
403
|
+
{
|
|
404
|
+
rating: input.rating,
|
|
405
|
+
title: input.title,
|
|
406
|
+
review: input.body,
|
|
407
|
+
storeFront: input.storeFront,
|
|
408
|
+
appVersionString: input.appVersionString,
|
|
409
|
+
lastModified: Date.now()
|
|
410
|
+
}
|
|
411
|
+
);
|
|
350
412
|
return {
|
|
351
413
|
content: [
|
|
352
414
|
{
|
|
@@ -426,9 +488,11 @@ var DIMENSION_INFO = {
|
|
|
426
488
|
};
|
|
427
489
|
async function discoverMetrics(input, client) {
|
|
428
490
|
try {
|
|
429
|
-
const response = await client.get(
|
|
430
|
-
|
|
431
|
-
|
|
491
|
+
const response = await client.get(
|
|
492
|
+
`/api/assets/${input.assetId}/metrics/availability`
|
|
493
|
+
);
|
|
494
|
+
const availableMetrics = response.data?.availableMetrics ?? [];
|
|
495
|
+
const metricGroups = response.data?.metricGroups ?? {};
|
|
432
496
|
const enriched = availableMetrics.map((metricName) => {
|
|
433
497
|
const info = METRIC_INFO[metricName];
|
|
434
498
|
return {
|
|
@@ -479,10 +543,13 @@ async function getMetrics(input, client) {
|
|
|
479
543
|
if (input.dimensionFilter && input.dimension) {
|
|
480
544
|
params[`filter.${input.dimension}`] = input.dimensionFilter;
|
|
481
545
|
}
|
|
482
|
-
const response = await client.get(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
546
|
+
const response = await client.get(
|
|
547
|
+
`/api/assets/${input.assetId}/metrics/timeseries`,
|
|
548
|
+
params
|
|
549
|
+
);
|
|
550
|
+
const data = response.data ?? {};
|
|
551
|
+
const changes = response.changes ?? {};
|
|
552
|
+
const meta = response.meta ?? {};
|
|
486
553
|
const summaries = {};
|
|
487
554
|
for (const [metricName, timeseries] of Object.entries(data)) {
|
|
488
555
|
const points = timeseries;
|
|
@@ -538,17 +605,20 @@ async function discoverDimensions(input, client) {
|
|
|
538
605
|
assetId: input.assetId,
|
|
539
606
|
dimension: input.dimension,
|
|
540
607
|
displayName: DIMENSION_INFO[input.dimension] || input.dimension,
|
|
541
|
-
values: response2.data
|
|
608
|
+
values: response2.data ?? response2
|
|
542
609
|
})
|
|
543
610
|
}]
|
|
544
611
|
};
|
|
545
612
|
}
|
|
546
613
|
const response = await client.get(`/api/assets/${input.assetId}/metrics/dimensions`);
|
|
547
|
-
const dimensions = response.data
|
|
548
|
-
const enriched =
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
614
|
+
const dimensions = Array.isArray(response.data) ? response.data : [];
|
|
615
|
+
const enriched = dimensions.map((dim) => {
|
|
616
|
+
if (typeof dim === "string") {
|
|
617
|
+
return { name: dim, displayName: DIMENSION_INFO[dim] || dim };
|
|
618
|
+
}
|
|
619
|
+
const id = dim.id ?? "";
|
|
620
|
+
return { name: id, displayName: dim.label ?? DIMENSION_INFO[id] ?? id };
|
|
621
|
+
});
|
|
552
622
|
return {
|
|
553
623
|
content: [{
|
|
554
624
|
type: "text",
|
|
@@ -566,6 +636,9 @@ async function discoverDimensions(input, client) {
|
|
|
566
636
|
|
|
567
637
|
// src/tools/agents.ts
|
|
568
638
|
import { z as z4 } from "zod";
|
|
639
|
+
function asRecord(value) {
|
|
640
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
641
|
+
}
|
|
569
642
|
var AGENT_TYPES = [
|
|
570
643
|
"review",
|
|
571
644
|
"monitoring",
|
|
@@ -614,7 +687,7 @@ async function listAgents(_input, client) {
|
|
|
614
687
|
let apiAgents = null;
|
|
615
688
|
try {
|
|
616
689
|
const response = await client.get("/api/agents");
|
|
617
|
-
apiAgents = response.data
|
|
690
|
+
apiAgents = Array.isArray(response) ? response : response.data ?? response.agents ?? null;
|
|
618
691
|
} catch {
|
|
619
692
|
}
|
|
620
693
|
if (apiAgents && Array.isArray(apiAgents)) {
|
|
@@ -664,21 +737,26 @@ async function getAgentDetails(input, client) {
|
|
|
664
737
|
const params = {};
|
|
665
738
|
if (input.assetId)
|
|
666
739
|
params.assetId = input.assetId;
|
|
667
|
-
const response2 = await client.get(`/api/agents/${input.agentType}`, params);
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
740
|
+
const response2 = await client.get(`/api/agents/by-type/${input.agentType}`, params);
|
|
741
|
+
if (response2.agent) {
|
|
742
|
+
return {
|
|
743
|
+
content: [
|
|
744
|
+
{
|
|
745
|
+
type: "text",
|
|
746
|
+
text: formatAsJson({
|
|
747
|
+
agent: response2.agent,
|
|
748
|
+
recentRuns: response2.recentRuns ?? []
|
|
749
|
+
})
|
|
750
|
+
}
|
|
751
|
+
]
|
|
752
|
+
};
|
|
753
|
+
}
|
|
676
754
|
} catch {
|
|
677
755
|
}
|
|
678
756
|
const response = {
|
|
679
757
|
type: input.agentType,
|
|
680
758
|
description: AGENT_DESCRIPTIONS[input.agentType],
|
|
681
|
-
message:
|
|
759
|
+
message: `No ${input.agentType} agent is provisioned for this organization yet. Activate it from the Agents page or with trigger_agent_run once created.`
|
|
682
760
|
};
|
|
683
761
|
return {
|
|
684
762
|
content: [
|
|
@@ -702,33 +780,44 @@ async function getAgentDetails(input, client) {
|
|
|
702
780
|
}
|
|
703
781
|
async function getAgentRunHistory(input, client) {
|
|
704
782
|
try {
|
|
783
|
+
let agentId;
|
|
705
784
|
try {
|
|
706
|
-
const
|
|
707
|
-
limit: input.limit
|
|
708
|
-
};
|
|
785
|
+
const byTypeParams = {};
|
|
709
786
|
if (input.assetId)
|
|
710
|
-
|
|
711
|
-
const
|
|
787
|
+
byTypeParams.assetId = input.assetId;
|
|
788
|
+
const byType = await client.get(
|
|
789
|
+
`/api/agents/by-type/${input.agentType}`,
|
|
790
|
+
byTypeParams
|
|
791
|
+
);
|
|
792
|
+
agentId = byType.agent?.id;
|
|
793
|
+
} catch {
|
|
794
|
+
}
|
|
795
|
+
if (!agentId) {
|
|
712
796
|
return {
|
|
713
797
|
content: [
|
|
714
798
|
{
|
|
715
799
|
type: "text",
|
|
716
800
|
text: formatAsJson({
|
|
717
801
|
agentType: input.agentType,
|
|
718
|
-
|
|
802
|
+
runs: [],
|
|
803
|
+
message: `No ${input.agentType} agent is provisioned for this organization yet, so there is no run history.`
|
|
719
804
|
})
|
|
720
805
|
}
|
|
721
806
|
]
|
|
722
807
|
};
|
|
723
|
-
} catch {
|
|
724
808
|
}
|
|
809
|
+
const response = await client.get(
|
|
810
|
+
`/api/agents/${agentId}/runs`,
|
|
811
|
+
{ limit: input.limit }
|
|
812
|
+
);
|
|
725
813
|
return {
|
|
726
814
|
content: [
|
|
727
815
|
{
|
|
728
816
|
type: "text",
|
|
729
817
|
text: formatAsJson({
|
|
730
818
|
agentType: input.agentType,
|
|
731
|
-
|
|
819
|
+
agentId,
|
|
820
|
+
...asRecord(response.data ?? response)
|
|
732
821
|
})
|
|
733
822
|
}
|
|
734
823
|
]
|
|
@@ -747,15 +836,16 @@ async function getAgentRunHistory(input, client) {
|
|
|
747
836
|
}
|
|
748
837
|
async function triggerAgentRun(input, client) {
|
|
749
838
|
try {
|
|
750
|
-
const body = {};
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
839
|
+
const body = input.assetId ? { triggerContext: { assetId: input.assetId } } : {};
|
|
840
|
+
const response = await client.post(
|
|
841
|
+
`/api/agents/${input.agentId}/run`,
|
|
842
|
+
body
|
|
843
|
+
);
|
|
754
844
|
return {
|
|
755
845
|
content: [
|
|
756
846
|
{
|
|
757
847
|
type: "text",
|
|
758
|
-
text: formatAsJson(response.data
|
|
848
|
+
text: formatAsJson(response.data ?? response)
|
|
759
849
|
}
|
|
760
850
|
]
|
|
761
851
|
};
|
|
@@ -773,12 +863,14 @@ async function triggerAgentRun(input, client) {
|
|
|
773
863
|
}
|
|
774
864
|
async function pauseAgent(input, client) {
|
|
775
865
|
try {
|
|
776
|
-
const response = await client.post(
|
|
866
|
+
const response = await client.post(
|
|
867
|
+
`/api/agents/${input.agentId}/pause`
|
|
868
|
+
);
|
|
777
869
|
return {
|
|
778
870
|
content: [
|
|
779
871
|
{
|
|
780
872
|
type: "text",
|
|
781
|
-
text: formatAsJson(response.data
|
|
873
|
+
text: formatAsJson(response.data ?? response)
|
|
782
874
|
}
|
|
783
875
|
]
|
|
784
876
|
};
|
|
@@ -796,12 +888,14 @@ async function pauseAgent(input, client) {
|
|
|
796
888
|
}
|
|
797
889
|
async function resumeAgent(input, client) {
|
|
798
890
|
try {
|
|
799
|
-
const response = await client.post(
|
|
891
|
+
const response = await client.post(
|
|
892
|
+
`/api/agents/${input.agentId}/resume`
|
|
893
|
+
);
|
|
800
894
|
return {
|
|
801
895
|
content: [
|
|
802
896
|
{
|
|
803
897
|
type: "text",
|
|
804
|
-
text: formatAsJson(response.data
|
|
898
|
+
text: formatAsJson(response.data ?? response)
|
|
805
899
|
}
|
|
806
900
|
]
|
|
807
901
|
};
|
|
@@ -819,12 +913,14 @@ async function resumeAgent(input, client) {
|
|
|
819
913
|
}
|
|
820
914
|
async function getAgentActivity(input, client) {
|
|
821
915
|
try {
|
|
822
|
-
const response = await client.get(
|
|
916
|
+
const response = await client.get(
|
|
917
|
+
`/api/agents/${input.agentId}/activity`
|
|
918
|
+
);
|
|
823
919
|
return {
|
|
824
920
|
content: [
|
|
825
921
|
{
|
|
826
922
|
type: "text",
|
|
827
|
-
text: formatAsJson(response.data
|
|
923
|
+
text: formatAsJson(response.data ?? response)
|
|
828
924
|
}
|
|
829
925
|
]
|
|
830
926
|
};
|
|
@@ -882,34 +978,40 @@ async function getAnomalies(input, client) {
|
|
|
882
978
|
params.fromDate = input.fromDate;
|
|
883
979
|
if (input.toDate)
|
|
884
980
|
params.toDate = input.toDate;
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
981
|
+
const response = await client.get(
|
|
982
|
+
"/api/anomalies",
|
|
983
|
+
params
|
|
984
|
+
);
|
|
985
|
+
let anomalies = response.data ?? response.anomalies ?? [];
|
|
986
|
+
if (input.excludeDismissed && !input.status) {
|
|
987
|
+
anomalies = anomalies.filter((a) => a.status !== "dismissed");
|
|
988
|
+
}
|
|
889
989
|
return {
|
|
890
990
|
content: [
|
|
891
991
|
{
|
|
892
992
|
type: "text",
|
|
893
993
|
text: formatAsJson({
|
|
894
994
|
total: anomalies.length,
|
|
895
|
-
anomalies: anomalies.map((
|
|
896
|
-
id:
|
|
897
|
-
assetId:
|
|
898
|
-
assetName:
|
|
899
|
-
assetIcon:
|
|
900
|
-
anomalyDate:
|
|
901
|
-
metricName:
|
|
902
|
-
sourceType:
|
|
903
|
-
type:
|
|
904
|
-
severity:
|
|
905
|
-
actualValue: parseFloat(
|
|
906
|
-
expectedValue: parseFloat(
|
|
907
|
-
deviationPercent: parseFloat(
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
995
|
+
anomalies: anomalies.map((anomaly) => ({
|
|
996
|
+
id: anomaly.id,
|
|
997
|
+
assetId: anomaly.assetId,
|
|
998
|
+
assetName: anomaly.assetName,
|
|
999
|
+
assetIcon: anomaly.assetIcon,
|
|
1000
|
+
anomalyDate: anomaly.anomalyDate,
|
|
1001
|
+
metricName: anomaly.metricName,
|
|
1002
|
+
sourceType: anomaly.sourceType,
|
|
1003
|
+
type: anomaly.type,
|
|
1004
|
+
severity: anomaly.severity,
|
|
1005
|
+
actualValue: parseFloat(String(anomaly.actualValue || "0")),
|
|
1006
|
+
expectedValue: parseFloat(String(anomaly.expectedValue || "0")),
|
|
1007
|
+
deviationPercent: parseFloat(
|
|
1008
|
+
String(anomaly.deviationPercent || "0")
|
|
1009
|
+
),
|
|
1010
|
+
confidence: parseFloat(String(anomaly.confidence || "0")),
|
|
1011
|
+
explanation: anomaly.explanation,
|
|
1012
|
+
suggestedActions: anomaly.suggestedActions,
|
|
1013
|
+
status: anomaly.status,
|
|
1014
|
+
detectedAt: anomaly.detectedAt
|
|
913
1015
|
}))
|
|
914
1016
|
})
|
|
915
1017
|
}
|
|
@@ -1008,37 +1110,46 @@ var getAdsPerformanceSchema = z6.object({
|
|
|
1008
1110
|
});
|
|
1009
1111
|
async function getAdsPerformance(input, client) {
|
|
1010
1112
|
try {
|
|
1011
|
-
const params = {
|
|
1012
|
-
limit: input.limit
|
|
1013
|
-
};
|
|
1113
|
+
const params = {};
|
|
1014
1114
|
if (input.assetId)
|
|
1015
1115
|
params.assetId = input.assetId;
|
|
1016
|
-
if (input.platform)
|
|
1017
|
-
params.platform = input.platform;
|
|
1018
1116
|
if (input.fromDate)
|
|
1019
|
-
params.
|
|
1117
|
+
params.from = input.fromDate;
|
|
1020
1118
|
if (input.toDate)
|
|
1021
|
-
params.
|
|
1022
|
-
const response = await client.get(
|
|
1023
|
-
|
|
1119
|
+
params.to = input.toDate;
|
|
1120
|
+
const response = await client.get(
|
|
1121
|
+
"/api/ads-agent/campaigns",
|
|
1122
|
+
params
|
|
1123
|
+
);
|
|
1124
|
+
let campaigns = response.data ?? response.campaigns ?? [];
|
|
1125
|
+
if (input.platform) {
|
|
1126
|
+
campaigns = campaigns.filter((c) => c.platform === input.platform);
|
|
1127
|
+
}
|
|
1128
|
+
campaigns = campaigns.slice(0, input.limit);
|
|
1024
1129
|
return {
|
|
1025
1130
|
content: [
|
|
1026
1131
|
{
|
|
1027
1132
|
type: "text",
|
|
1028
1133
|
text: formatAsJson({
|
|
1029
1134
|
total: campaigns.length,
|
|
1030
|
-
campaigns: campaigns.map((
|
|
1031
|
-
id:
|
|
1032
|
-
assetId:
|
|
1033
|
-
assetName:
|
|
1034
|
-
platformCampaignId:
|
|
1035
|
-
name:
|
|
1036
|
-
status:
|
|
1037
|
-
objective:
|
|
1038
|
-
platform:
|
|
1039
|
-
linkSource:
|
|
1040
|
-
linkedAt:
|
|
1041
|
-
|
|
1135
|
+
campaigns: campaigns.map((campaign) => ({
|
|
1136
|
+
id: campaign.id,
|
|
1137
|
+
assetId: campaign.assetId,
|
|
1138
|
+
assetName: campaign.assetName,
|
|
1139
|
+
platformCampaignId: campaign.platformCampaignId,
|
|
1140
|
+
name: campaign.name,
|
|
1141
|
+
status: campaign.status,
|
|
1142
|
+
objective: campaign.objective,
|
|
1143
|
+
platform: campaign.platform,
|
|
1144
|
+
linkSource: campaign.linkSource,
|
|
1145
|
+
linkedAt: campaign.linkedAt,
|
|
1146
|
+
totalSpend: campaign.totalSpend ?? null,
|
|
1147
|
+
totalImpressions: campaign.totalImpressions ?? null,
|
|
1148
|
+
totalTaps: campaign.totalTaps ?? null,
|
|
1149
|
+
totalInstalls: campaign.totalInstalls ?? null,
|
|
1150
|
+
avgCPI: campaign.avgCPI ?? null,
|
|
1151
|
+
avgTTR: campaign.avgTTR ?? null,
|
|
1152
|
+
avgCVR: campaign.avgCVR ?? null
|
|
1042
1153
|
}))
|
|
1043
1154
|
})
|
|
1044
1155
|
}
|
|
@@ -1116,13 +1227,17 @@ async function getGrowthScore(input, client) {
|
|
|
1116
1227
|
import { z as z8 } from "zod";
|
|
1117
1228
|
var getForecastsSchema = z8.object({
|
|
1118
1229
|
assetId: z8.string().uuid().describe("App UUID to get forecasts for"),
|
|
1119
|
-
|
|
1230
|
+
metric: z8.enum(["proceeds", "total_revenue", "total_downloads", "active_subs", "new_trials"]).describe("The metric to forecast"),
|
|
1231
|
+
forecastWeeks: z8.number().int().min(4).max(156).optional().describe("Weeks to forecast forward (default 104)"),
|
|
1232
|
+
historicalWeeks: z8.number().int().min(12).max(156).optional().describe("Weeks of history to base the forecast on (default 104)")
|
|
1120
1233
|
});
|
|
1121
1234
|
async function getForecasts(input, client) {
|
|
1122
1235
|
try {
|
|
1123
1236
|
const response = await client.get("/api/forecasting/forecast", {
|
|
1124
1237
|
assetId: input.assetId,
|
|
1125
|
-
|
|
1238
|
+
metricName: input.metric,
|
|
1239
|
+
forecastWeeks: input.forecastWeeks,
|
|
1240
|
+
historicalWeeks: input.historicalWeeks
|
|
1126
1241
|
});
|
|
1127
1242
|
return {
|
|
1128
1243
|
content: [
|
|
@@ -1147,18 +1262,34 @@ async function getForecasts(input, client) {
|
|
|
1147
1262
|
|
|
1148
1263
|
// src/tools/dashboard.ts
|
|
1149
1264
|
import { z as z9 } from "zod";
|
|
1265
|
+
function parseDashboardNumber(value) {
|
|
1266
|
+
if (value === null || value === void 0 || value === "")
|
|
1267
|
+
return null;
|
|
1268
|
+
const parsed = typeof value === "number" ? value : Number.parseFloat(value);
|
|
1269
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1270
|
+
}
|
|
1271
|
+
function getSidebarAssets(response) {
|
|
1272
|
+
if (!response)
|
|
1273
|
+
return [];
|
|
1274
|
+
if (Array.isArray(response))
|
|
1275
|
+
return response;
|
|
1276
|
+
return response.data ?? [];
|
|
1277
|
+
}
|
|
1150
1278
|
var getDashboardOverviewSchema = z9.object({});
|
|
1151
1279
|
async function getDashboardOverview(_input, client) {
|
|
1152
1280
|
try {
|
|
1281
|
+
const overviewPromise = client.get("/api/dashboard/overview-metrics").catch(() => null);
|
|
1282
|
+
const sidebarPromise = client.get(
|
|
1283
|
+
"/api/dashboard/sidebar-assets"
|
|
1284
|
+
).catch(() => null);
|
|
1153
1285
|
const [overviewResponse, sidebarResponse] = await Promise.all([
|
|
1154
|
-
|
|
1155
|
-
|
|
1286
|
+
overviewPromise,
|
|
1287
|
+
sidebarPromise
|
|
1156
1288
|
]);
|
|
1157
|
-
const overview = overviewResponse?.data
|
|
1158
|
-
const
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
return sum + parseFloat(a.currentValuation || "0");
|
|
1289
|
+
const overview = overviewResponse?.data ?? overviewResponse ?? {};
|
|
1290
|
+
const apps = getSidebarAssets(sidebarResponse);
|
|
1291
|
+
const totalValuation = apps.reduce((sum, app) => {
|
|
1292
|
+
return sum + (parseDashboardNumber(app.currentValuation) ?? 0);
|
|
1162
1293
|
}, 0);
|
|
1163
1294
|
return {
|
|
1164
1295
|
content: [
|
|
@@ -1168,16 +1299,16 @@ async function getDashboardOverview(_input, client) {
|
|
|
1168
1299
|
portfolio: {
|
|
1169
1300
|
totalApps: apps.length,
|
|
1170
1301
|
totalValuation: totalValuation > 0 ? totalValuation : null,
|
|
1171
|
-
apps: apps.map((
|
|
1172
|
-
id:
|
|
1173
|
-
name:
|
|
1174
|
-
bundleId:
|
|
1175
|
-
platform:
|
|
1176
|
-
currentValuation:
|
|
1177
|
-
rating:
|
|
1178
|
-
ratingCount:
|
|
1179
|
-
category:
|
|
1180
|
-
iconUrl:
|
|
1302
|
+
apps: apps.map((app) => ({
|
|
1303
|
+
id: app.id,
|
|
1304
|
+
name: app.name,
|
|
1305
|
+
bundleId: app.bundleId,
|
|
1306
|
+
platform: app.appleAppId ? "ios" : app.googleAppId ? "android" : "unknown",
|
|
1307
|
+
currentValuation: parseDashboardNumber(app.currentValuation),
|
|
1308
|
+
rating: parseDashboardNumber(app.rating),
|
|
1309
|
+
ratingCount: app.ratingCount ?? null,
|
|
1310
|
+
category: app.category ?? null,
|
|
1311
|
+
iconUrl: app.iconUrl ?? null
|
|
1181
1312
|
}))
|
|
1182
1313
|
},
|
|
1183
1314
|
overview
|
|
@@ -1200,13 +1331,15 @@ async function getDashboardOverview(_input, client) {
|
|
|
1200
1331
|
|
|
1201
1332
|
// src/tools/actions.ts
|
|
1202
1333
|
import { z as z10 } from "zod";
|
|
1334
|
+
function asRecord2(value) {
|
|
1335
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1336
|
+
}
|
|
1203
1337
|
var listPendingActionsSchema = z10.object({
|
|
1204
1338
|
assetId: z10.string().uuid().optional().describe("Filter by app UUID"),
|
|
1205
1339
|
limit: z10.number().int().min(1).max(100).default(50).describe("Maximum number of actions to return")
|
|
1206
1340
|
});
|
|
1207
1341
|
var approveActionSchema = z10.object({
|
|
1208
|
-
actionId: z10.string().describe("The
|
|
1209
|
-
editedReply: z10.string().optional().describe("Optionally modify the reply text before approving")
|
|
1342
|
+
actionId: z10.string().describe("The pending action ID to approve")
|
|
1210
1343
|
});
|
|
1211
1344
|
var rejectActionSchema = z10.object({
|
|
1212
1345
|
actionId: z10.string().describe("The draft reply ID to reject/delete")
|
|
@@ -1216,10 +1349,16 @@ async function listPendingActions(input, client) {
|
|
|
1216
1349
|
const params = {
|
|
1217
1350
|
limit: input.limit
|
|
1218
1351
|
};
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1352
|
+
const response = await client.get(
|
|
1353
|
+
"/api/pending-actions",
|
|
1354
|
+
params
|
|
1355
|
+
);
|
|
1356
|
+
let actions = response.data ?? response.actions ?? [];
|
|
1357
|
+
if (input.assetId) {
|
|
1358
|
+
actions = actions.filter(
|
|
1359
|
+
(action) => action.assetId === input.assetId
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1223
1362
|
return {
|
|
1224
1363
|
content: [
|
|
1225
1364
|
{
|
|
@@ -1245,10 +1384,10 @@ async function listPendingActions(input, client) {
|
|
|
1245
1384
|
}
|
|
1246
1385
|
async function approveAction(input, client) {
|
|
1247
1386
|
try {
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1387
|
+
const response = await client.post(
|
|
1388
|
+
`/api/pending-actions/${input.actionId}/approve`,
|
|
1389
|
+
{}
|
|
1390
|
+
);
|
|
1252
1391
|
return {
|
|
1253
1392
|
content: [
|
|
1254
1393
|
{
|
|
@@ -1258,7 +1397,7 @@ async function approveAction(input, client) {
|
|
|
1258
1397
|
actionId: input.actionId,
|
|
1259
1398
|
status: "approved",
|
|
1260
1399
|
message: "Reply has been approved and queued for sending",
|
|
1261
|
-
...response.data
|
|
1400
|
+
...asRecord2(response.data)
|
|
1262
1401
|
})
|
|
1263
1402
|
}
|
|
1264
1403
|
]
|
|
@@ -1277,7 +1416,9 @@ async function approveAction(input, client) {
|
|
|
1277
1416
|
}
|
|
1278
1417
|
async function rejectAction(input, client) {
|
|
1279
1418
|
try {
|
|
1280
|
-
const response = await client.post(
|
|
1419
|
+
const response = await client.post(
|
|
1420
|
+
`/api/pending-actions/${input.actionId}/reject`
|
|
1421
|
+
);
|
|
1281
1422
|
return {
|
|
1282
1423
|
content: [
|
|
1283
1424
|
{
|
|
@@ -1287,7 +1428,7 @@ async function rejectAction(input, client) {
|
|
|
1287
1428
|
actionId: input.actionId,
|
|
1288
1429
|
status: "rejected",
|
|
1289
1430
|
message: "Draft reply has been rejected and removed",
|
|
1290
|
-
...response.data
|
|
1431
|
+
...asRecord2(response.data)
|
|
1291
1432
|
})
|
|
1292
1433
|
}
|
|
1293
1434
|
]
|
|
@@ -1321,10 +1462,13 @@ var getAsoExperimentsSchema = z11.object({
|
|
|
1321
1462
|
assetId: z11.string().uuid().describe("App UUID to list ASO experiments for"),
|
|
1322
1463
|
status: z11.enum(["proposed", "approved", "applied", "measuring", "completed", "reverted"]).optional().describe("Filter experiments by status"),
|
|
1323
1464
|
limit: z11.number().int().min(1).max(100).default(20).describe("Maximum number of experiments to return"),
|
|
1324
|
-
|
|
1465
|
+
cursor: z11.string().optional().describe("Pagination cursor from a previous response (meta.nextCursor)")
|
|
1325
1466
|
});
|
|
1326
1467
|
var getAsoLocaleSnapshotsSchema = z11.object({
|
|
1327
|
-
assetId: z11.string().uuid().describe("App UUID to get locale snapshots for")
|
|
1468
|
+
assetId: z11.string().uuid().describe("App UUID to get locale snapshots for"),
|
|
1469
|
+
store: z11.enum(["ios", "android"]).default("ios").describe("Which store listing to read"),
|
|
1470
|
+
locale: z11.string().optional().describe('Filter to a single locale (e.g. "en-US"). Returns all locales if omitted.'),
|
|
1471
|
+
limit: z11.number().int().min(1).max(500).optional().describe("Maximum snapshot rows to return (newest first)")
|
|
1328
1472
|
});
|
|
1329
1473
|
var triggerAsoAnalysisSchema = z11.object({
|
|
1330
1474
|
assetId: z11.string().uuid().describe("App UUID to trigger ASO analysis for")
|
|
@@ -1405,7 +1549,7 @@ async function getAsoExperiments(input, client) {
|
|
|
1405
1549
|
try {
|
|
1406
1550
|
const params = {
|
|
1407
1551
|
limit: input.limit,
|
|
1408
|
-
|
|
1552
|
+
cursor: input.cursor
|
|
1409
1553
|
};
|
|
1410
1554
|
if (input.status)
|
|
1411
1555
|
params.status = input.status;
|
|
@@ -1432,7 +1576,14 @@ async function getAsoExperiments(input, client) {
|
|
|
1432
1576
|
}
|
|
1433
1577
|
async function getAsoLocaleSnapshots(input, client) {
|
|
1434
1578
|
try {
|
|
1435
|
-
const response = await client.get(
|
|
1579
|
+
const response = await client.get(
|
|
1580
|
+
`/api/assets/${input.assetId}/aso/listing-snapshots`,
|
|
1581
|
+
{
|
|
1582
|
+
store: input.store,
|
|
1583
|
+
locale: input.locale,
|
|
1584
|
+
limit: input.limit
|
|
1585
|
+
}
|
|
1586
|
+
);
|
|
1436
1587
|
return {
|
|
1437
1588
|
content: [
|
|
1438
1589
|
{
|
|
@@ -1455,7 +1606,10 @@ async function getAsoLocaleSnapshots(input, client) {
|
|
|
1455
1606
|
}
|
|
1456
1607
|
async function triggerAsoAnalysis(input, client) {
|
|
1457
1608
|
try {
|
|
1458
|
-
const response = await client.post(
|
|
1609
|
+
const response = await client.post(
|
|
1610
|
+
`/api/assets/${input.assetId}/aso/refetch`,
|
|
1611
|
+
{ force: true }
|
|
1612
|
+
);
|
|
1459
1613
|
return {
|
|
1460
1614
|
content: [
|
|
1461
1615
|
{
|
|
@@ -1478,6 +1632,7 @@ async function triggerAsoAnalysis(input, client) {
|
|
|
1478
1632
|
}
|
|
1479
1633
|
|
|
1480
1634
|
// src/tools/chat.ts
|
|
1635
|
+
import { randomUUID } from "node:crypto";
|
|
1481
1636
|
import { z as z12 } from "zod";
|
|
1482
1637
|
var listConversationsSchema = z12.object({
|
|
1483
1638
|
limit: z12.number().int().min(1).max(100).default(20).describe("Maximum number of conversations to return")
|
|
@@ -1547,23 +1702,81 @@ async function getConversationMessages(input, client) {
|
|
|
1547
1702
|
};
|
|
1548
1703
|
}
|
|
1549
1704
|
}
|
|
1705
|
+
function parseUiMessageStream(raw) {
|
|
1706
|
+
let reply = "";
|
|
1707
|
+
const toolsUsed = [];
|
|
1708
|
+
for (const line of raw.split("\n")) {
|
|
1709
|
+
if (!line.startsWith("data:"))
|
|
1710
|
+
continue;
|
|
1711
|
+
const payload = line.slice(5).trim();
|
|
1712
|
+
if (!payload || payload === "[DONE]")
|
|
1713
|
+
continue;
|
|
1714
|
+
try {
|
|
1715
|
+
const event = JSON.parse(payload);
|
|
1716
|
+
if (event.type === "text-delta") {
|
|
1717
|
+
reply += String(event.delta ?? event.textDelta ?? "");
|
|
1718
|
+
} else if (typeof event.type === "string" && event.type === "tool-input-start" && typeof event.toolName === "string") {
|
|
1719
|
+
toolsUsed.push(event.toolName);
|
|
1720
|
+
}
|
|
1721
|
+
} catch {
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return { reply, toolsUsed };
|
|
1725
|
+
}
|
|
1550
1726
|
async function sendChatMessage(input, client) {
|
|
1551
1727
|
try {
|
|
1728
|
+
const conversationId = input.conversationId ?? randomUUID();
|
|
1729
|
+
let priorMessages = [];
|
|
1730
|
+
if (input.conversationId) {
|
|
1731
|
+
const history = await client.get(`/api/chat/messages/${input.conversationId}`, { limit: 1e3 });
|
|
1732
|
+
for (const message of history.data ?? []) {
|
|
1733
|
+
const textParts = (message.parts ?? []).filter(
|
|
1734
|
+
(part) => part.type === "text" && typeof part.text === "string"
|
|
1735
|
+
);
|
|
1736
|
+
if (textParts.length === 0)
|
|
1737
|
+
continue;
|
|
1738
|
+
priorMessages.push({
|
|
1739
|
+
id: message.id,
|
|
1740
|
+
role: message.role,
|
|
1741
|
+
parts: textParts
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
priorMessages = priorMessages.slice(-100);
|
|
1745
|
+
}
|
|
1552
1746
|
const body = {
|
|
1553
|
-
|
|
1747
|
+
id: conversationId,
|
|
1748
|
+
messages: [
|
|
1749
|
+
...priorMessages,
|
|
1750
|
+
{
|
|
1751
|
+
id: randomUUID(),
|
|
1752
|
+
role: "user",
|
|
1753
|
+
parts: [{ type: "text", text: input.message }]
|
|
1754
|
+
}
|
|
1755
|
+
]
|
|
1554
1756
|
};
|
|
1555
|
-
if (input.
|
|
1556
|
-
body.
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1757
|
+
if (input.agentType) {
|
|
1758
|
+
body.agentContext = {
|
|
1759
|
+
agentId: null,
|
|
1760
|
+
agentType: input.agentType,
|
|
1761
|
+
agentName: input.agentType,
|
|
1762
|
+
surface: "mcp"
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
const raw = await client.postStream("/api/chat", body);
|
|
1766
|
+
const { reply, toolsUsed } = parseUiMessageStream(raw);
|
|
1560
1767
|
return {
|
|
1561
1768
|
content: [
|
|
1562
1769
|
{
|
|
1563
1770
|
type: "text",
|
|
1564
|
-
text: formatAsJson(
|
|
1771
|
+
text: formatAsJson({
|
|
1772
|
+
conversationId,
|
|
1773
|
+
reply: reply || "The assistant returned no text response.",
|
|
1774
|
+
toolsUsed,
|
|
1775
|
+
tip: "Pass this conversationId to continue the conversation or to get_conversation_messages for the full transcript."
|
|
1776
|
+
})
|
|
1565
1777
|
}
|
|
1566
|
-
]
|
|
1778
|
+
],
|
|
1779
|
+
...reply ? {} : { isError: true }
|
|
1567
1780
|
};
|
|
1568
1781
|
} catch (error) {
|
|
1569
1782
|
return {
|
|
@@ -1581,8 +1794,8 @@ async function sendChatMessage(input, client) {
|
|
|
1581
1794
|
// src/tools/index.ts
|
|
1582
1795
|
function registerTools(server, client, options = {}) {
|
|
1583
1796
|
const rateLimiter = options.rateLimiter ?? new RateLimiter(100, 6e4);
|
|
1584
|
-
function wrapTool(
|
|
1585
|
-
|
|
1797
|
+
function wrapTool(handler) {
|
|
1798
|
+
const callback = async (input) => {
|
|
1586
1799
|
const rateCheck = rateLimiter.check("default");
|
|
1587
1800
|
if (!rateCheck.allowed) {
|
|
1588
1801
|
return {
|
|
@@ -1599,6 +1812,7 @@ function registerTools(server, client, options = {}) {
|
|
|
1599
1812
|
}
|
|
1600
1813
|
return handler(input, client);
|
|
1601
1814
|
};
|
|
1815
|
+
return callback;
|
|
1602
1816
|
}
|
|
1603
1817
|
function tool(name, title, description, schema, handler, annotations) {
|
|
1604
1818
|
server.registerTool(
|
|
@@ -1609,8 +1823,7 @@ function registerTools(server, client, options = {}) {
|
|
|
1609
1823
|
inputSchema: schema.shape,
|
|
1610
1824
|
annotations: { title, ...annotations }
|
|
1611
1825
|
},
|
|
1612
|
-
|
|
1613
|
-
wrapTool(name, handler)
|
|
1826
|
+
wrapTool(handler)
|
|
1614
1827
|
);
|
|
1615
1828
|
}
|
|
1616
1829
|
tool(
|
|
@@ -1656,7 +1869,7 @@ function registerTools(server, client, options = {}) {
|
|
|
1656
1869
|
tool(
|
|
1657
1870
|
"translate_review",
|
|
1658
1871
|
"Translate review",
|
|
1659
|
-
"Translate a review to English. Useful for reviews written in other languages. Returns the translated text. Does not modify the review in Fload.",
|
|
1872
|
+
"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.",
|
|
1660
1873
|
translateReviewSchema,
|
|
1661
1874
|
translateReview,
|
|
1662
1875
|
{ readOnlyHint: true, openWorldHint: true }
|
|
@@ -1800,7 +2013,7 @@ function registerTools(server, client, options = {}) {
|
|
|
1800
2013
|
tool(
|
|
1801
2014
|
"get_forecasts",
|
|
1802
2015
|
"Get forecasts",
|
|
1803
|
-
"Get
|
|
2016
|
+
"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.",
|
|
1804
2017
|
getForecastsSchema,
|
|
1805
2018
|
getForecasts,
|
|
1806
2019
|
{ readOnlyHint: true, openWorldHint: false }
|
|
@@ -1923,6 +2136,29 @@ async function createMcpServer(_config, client) {
|
|
|
1923
2136
|
return server;
|
|
1924
2137
|
}
|
|
1925
2138
|
|
|
2139
|
+
// src/public-errors.ts
|
|
2140
|
+
var INTERNAL_ERROR_PATTERNS = [
|
|
2141
|
+
/\b(stack|trace|at\s+[\w.$<>]+\s*\()/i,
|
|
2142
|
+
/\b(sql|postgres|drizzle|database|constraint|duplicate key|violates|relation)\b/i,
|
|
2143
|
+
/\b(fetch|undici|socket|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|HTTP\s+\d{3})\b/i,
|
|
2144
|
+
/\b(token|secret|api[_-]?key|authorization|bearer|cookie|password)\b/i,
|
|
2145
|
+
/\b[a-z0-9]+_(secret|token|key)\b/i,
|
|
2146
|
+
/\b(secret|token|key)[_-][a-z0-9]+\b/i,
|
|
2147
|
+
/\b(scrap|browser|patchright|playwright|chromium|page\.|locator|selector)\b/i,
|
|
2148
|
+
/\b(redis|bullmq|queue|job id|worker)\b/i
|
|
2149
|
+
];
|
|
2150
|
+
function looksInternal(message) {
|
|
2151
|
+
return INTERNAL_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
2152
|
+
}
|
|
2153
|
+
function publicMcpErrorMessage(message, fallbackMessage = "Fload could not complete the request. Please try again.") {
|
|
2154
|
+
const raw = message instanceof Error ? message.message : typeof message === "string" ? message : "";
|
|
2155
|
+
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
2156
|
+
if (!normalized || normalized.length > 180 || looksInternal(normalized)) {
|
|
2157
|
+
return fallbackMessage;
|
|
2158
|
+
}
|
|
2159
|
+
return normalized;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
1926
2162
|
// src/api-client.ts
|
|
1927
2163
|
var FloadApiClient = class {
|
|
1928
2164
|
constructor(baseUrl, apiKey) {
|
|
@@ -1931,17 +2167,22 @@ var FloadApiClient = class {
|
|
|
1931
2167
|
}
|
|
1932
2168
|
async request(path, options = {}) {
|
|
1933
2169
|
const url = `${this.baseUrl}${path}`;
|
|
2170
|
+
const headers = {
|
|
2171
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
2172
|
+
...options.body ? { "Content-Type": "application/json" } : {},
|
|
2173
|
+
...options.headers
|
|
2174
|
+
};
|
|
1934
2175
|
const response = await fetch(url, {
|
|
1935
2176
|
...options,
|
|
1936
|
-
headers
|
|
1937
|
-
"Authorization": `Bearer ${this.apiKey}`,
|
|
1938
|
-
"Content-Type": "application/json",
|
|
1939
|
-
...options.headers
|
|
1940
|
-
}
|
|
2177
|
+
headers
|
|
1941
2178
|
});
|
|
1942
2179
|
if (!response.ok) {
|
|
1943
2180
|
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
1944
|
-
|
|
2181
|
+
const safeError = publicMcpErrorMessage(
|
|
2182
|
+
error.publicMessage || error.message || response.statusText,
|
|
2183
|
+
response.status >= 500 ? "Fload could not complete the request. Please try again." : "Fload rejected the request. Please check access and try again."
|
|
2184
|
+
);
|
|
2185
|
+
throw new Error(safeError);
|
|
1945
2186
|
}
|
|
1946
2187
|
return response.json();
|
|
1947
2188
|
}
|
|
@@ -1971,6 +2212,31 @@ var FloadApiClient = class {
|
|
|
1971
2212
|
async delete(path) {
|
|
1972
2213
|
return this.request(path, { method: "DELETE" });
|
|
1973
2214
|
}
|
|
2215
|
+
/**
|
|
2216
|
+
* POST to a streaming endpoint (SSE / AI SDK UI-message stream) and
|
|
2217
|
+
* return the raw response body as text. Callers parse the event lines.
|
|
2218
|
+
*/
|
|
2219
|
+
async postStream(path, body) {
|
|
2220
|
+
const url = `${this.baseUrl}${path}`;
|
|
2221
|
+
const response = await fetch(url, {
|
|
2222
|
+
method: "POST",
|
|
2223
|
+
headers: {
|
|
2224
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
2225
|
+
"Content-Type": "application/json",
|
|
2226
|
+
Accept: "text/event-stream"
|
|
2227
|
+
},
|
|
2228
|
+
body: body ? JSON.stringify(body) : void 0
|
|
2229
|
+
});
|
|
2230
|
+
if (!response.ok) {
|
|
2231
|
+
const error = await response.json().catch(() => ({ message: response.statusText }));
|
|
2232
|
+
const safeError = publicMcpErrorMessage(
|
|
2233
|
+
error.publicMessage || error.message || response.statusText,
|
|
2234
|
+
response.status >= 500 ? "Fload could not complete the request. Please try again." : "Fload rejected the request. Please check access and try again."
|
|
2235
|
+
);
|
|
2236
|
+
throw new Error(safeError);
|
|
2237
|
+
}
|
|
2238
|
+
return response.text();
|
|
2239
|
+
}
|
|
1974
2240
|
};
|
|
1975
2241
|
|
|
1976
2242
|
// src/bin.ts
|