@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.
@@ -53,9 +53,12 @@ async function listApps(input, client) {
53
53
  try {
54
54
  const response = await client.get("/api/assets", {
55
55
  limit: input.limit,
56
- offset: 0
56
+ offset: 0,
57
+ // The listing hides assets without a valuation by default; "list all
58
+ // apps" must include apps that haven't been valued yet (FLO-734).
59
+ includeUnvalued: "true"
57
60
  });
58
- const apps = response.data || [];
61
+ const apps = response.data ?? [];
59
62
  const filtered = input.platform ? apps.filter((app) => {
60
63
  if (input.platform === "ios")
61
64
  return !!app.appleAppId;
@@ -99,8 +102,14 @@ async function getAppDetails(input, client) {
99
102
  }
100
103
  let assetId = input.assetId;
101
104
  if (!assetId && input.bundleId) {
102
- const allApps = await client.get("/api/assets", { limit: 100, offset: 0 });
103
- const match = (allApps.data || []).find((a) => a.bundleId === input.bundleId);
105
+ const allApps = await client.get("/api/assets", {
106
+ limit: 100,
107
+ offset: 0,
108
+ includeUnvalued: "true"
109
+ });
110
+ const match = (allApps.data ?? []).find(
111
+ (app2) => app2.bundleId === input.bundleId
112
+ );
104
113
  if (!match) {
105
114
  return {
106
115
  content: [
@@ -114,8 +123,13 @@ async function getAppDetails(input, client) {
114
123
  }
115
124
  assetId = match.id;
116
125
  }
117
- const response = await client.get(`/api/assets/${assetId}`);
118
- const app = response.data || response;
126
+ const response = await client.get(
127
+ `/api/assets/${assetId}`
128
+ );
129
+ const app = response.data;
130
+ if (!app) {
131
+ throw new Error("App details response did not include app data");
132
+ }
119
133
  return {
120
134
  content: [
121
135
  {
@@ -172,14 +186,27 @@ var sendReviewReplySchema = z2.object({
172
186
  });
173
187
  var translateReviewSchema = z2.object({
174
188
  reviewId: z2.string().describe("The review UUID to translate"),
175
- assetId: z2.string().uuid().describe("The app UUID the review belongs to")
189
+ assetId: z2.string().uuid().describe("The app UUID the review belongs to"),
190
+ // The translate endpoint receives the review content in the request body
191
+ // (it does not re-fetch it). Pass the fields from a get_reviews result.
192
+ rating: z2.number().int().min(1).max(5).describe("The review star rating, from get_reviews"),
193
+ title: z2.string().default("").describe("The review title, from get_reviews"),
194
+ body: z2.string().describe("The review text to translate, from get_reviews"),
195
+ storeFront: z2.string().default("").describe('The review storefront (e.g. "SE"), from get_reviews'),
196
+ appVersionString: z2.string().default("").describe("The app version the review was left on, from get_reviews")
176
197
  });
177
198
  async function getReviews(input, client) {
178
199
  try {
179
200
  let assetId = input.assetId;
180
201
  if (!assetId && input.bundleId) {
181
- const allApps = await client.get("/api/assets", { limit: 100, offset: 0 });
182
- const match = (allApps.data || []).find((a) => a.bundleId === input.bundleId);
202
+ const allApps = await client.get("/api/assets", {
203
+ limit: 100,
204
+ offset: 0,
205
+ includeUnvalued: "true"
206
+ });
207
+ const match = (allApps.data ?? []).find(
208
+ (app) => app.bundleId === input.bundleId
209
+ );
183
210
  if (!match) {
184
211
  return {
185
212
  content: [
@@ -193,12 +220,20 @@ async function getReviews(input, client) {
193
220
  }
194
221
  assetId = match.id;
195
222
  }
223
+ if (!assetId) {
224
+ return {
225
+ content: [
226
+ {
227
+ type: "text",
228
+ text: "Provide an assetId (or bundleId) to fetch reviews for. Use list_apps to find app IDs."
229
+ }
230
+ ],
231
+ isError: true
232
+ };
233
+ }
196
234
  const params = {
197
- limit: input.limit,
198
- sortBy: input.sortBy
235
+ limit: input.limit
199
236
  };
200
- if (assetId)
201
- params.assetId = assetId;
202
237
  if (input.platform)
203
238
  params.platform = input.platform;
204
239
  if (input.rating !== void 0)
@@ -209,13 +244,25 @@ async function getReviews(input, client) {
209
244
  params.startDate = input.startDate;
210
245
  if (input.endDate)
211
246
  params.endDate = input.endDate;
212
- const response = await client.get("/api/reviews", params);
213
- const reviews = response.data || response.reviews || [];
247
+ const response = await client.get(
248
+ `/api/assets/${assetId}/reviews`,
249
+ params
250
+ );
251
+ let reviews = response.data?.reviews ?? [];
252
+ if (input.sortBy === "rating") {
253
+ reviews = [...reviews].sort(
254
+ (a, b) => Number(b.rating ?? 0) - Number(a.rating ?? 0)
255
+ );
256
+ }
257
+ const hasReply = (review) => !!(review.developerResponse || review.replySource || review.hasReply);
214
258
  const summary = {
215
259
  totalReviews: reviews.length,
216
- averageRating: reviews.length > 0 ? (reviews.reduce((sum, r) => sum + (r.rating || 0), 0) / reviews.length).toFixed(2) : 0,
217
- repliedCount: reviews.filter((r) => r.developerResponse || r.hasReply).length,
218
- unrepliedCount: reviews.filter((r) => !r.developerResponse && !r.hasReply).length
260
+ averageRating: reviews.length > 0 ? (reviews.reduce((sum, review) => {
261
+ const rating = Number(review.rating ?? 0);
262
+ return sum + (Number.isFinite(rating) ? rating : 0);
263
+ }, 0) / reviews.length).toFixed(2) : 0,
264
+ repliedCount: reviews.filter((review) => hasReply(review)).length,
265
+ unrepliedCount: reviews.filter((review) => !hasReply(review)).length
219
266
  };
220
267
  return {
221
268
  content: [
@@ -223,19 +270,23 @@ async function getReviews(input, client) {
223
270
  type: "text",
224
271
  text: formatAsJson({
225
272
  summary,
226
- reviews: reviews.map((r) => ({
227
- id: r.id,
228
- appId: r.appId,
229
- platform: r.platform,
230
- rating: r.rating,
231
- title: r.title,
232
- body: r.body,
233
- author: r.nickname || r.author,
234
- date: r.lastModified || r.date,
235
- version: r.appVersionString || r.version,
236
- storeFront: r.storeFront,
237
- hasReply: !!(r.developerResponse || r.hasReply),
238
- reply: r.developerResponse ? typeof r.developerResponse === "object" ? r.developerResponse.response : r.developerResponse : r.reply || null
273
+ reviews: reviews.map((review) => ({
274
+ id: review.id,
275
+ appId: review.appId,
276
+ platform: review.platform,
277
+ rating: review.rating,
278
+ title: review.title,
279
+ // The handler exposes the review text as `review`.
280
+ body: review.review ?? review.body,
281
+ author: review.nickname || review.author,
282
+ // Epoch ms from the handler.
283
+ date: review.lastModified || review.date,
284
+ version: review.appVersionString || review.version,
285
+ storeFront: review.storeFront,
286
+ hasReply: hasReply(review),
287
+ replySource: review.replySource ?? null,
288
+ hasDraft: review.hasDraft ?? false,
289
+ reply: review.developerResponse ? typeof review.developerResponse === "object" ? review.developerResponse.response : review.developerResponse : review.reply || null
239
290
  }))
240
291
  })
241
292
  }
@@ -255,9 +306,10 @@ async function getReviews(input, client) {
255
306
  }
256
307
  async function generateReviewReply(input, client) {
257
308
  try {
258
- const response = await client.post(`/api/reviews/${input.reviewId}/generate-reply`, {
259
- assetId: input.assetId
260
- });
309
+ const response = await client.post(
310
+ `/api/assets/${input.assetId}/reviews/${input.reviewId}/generate-reply`,
311
+ void 0
312
+ );
261
313
  return {
262
314
  content: [
263
315
  {
@@ -280,10 +332,11 @@ async function generateReviewReply(input, client) {
280
332
  }
281
333
  async function sendReviewReply(input, client) {
282
334
  try {
283
- const response = await client.post(`/api/reviews/${input.reviewId}/respond`, {
284
- assetId: input.assetId,
285
- response: input.response
286
- });
335
+ const response = await client.post(
336
+ `/api/assets/${input.assetId}/reviews/${input.reviewId}/respond`,
337
+ // The API handler reads `responseText` from the body.
338
+ { responseText: input.response }
339
+ );
287
340
  return {
288
341
  content: [
289
342
  {
@@ -306,9 +359,18 @@ async function sendReviewReply(input, client) {
306
359
  }
307
360
  async function translateReview(input, client) {
308
361
  try {
309
- const response = await client.post(`/api/reviews/${input.reviewId}/translate`, {
310
- assetId: input.assetId
311
- });
362
+ const response = await client.post(
363
+ `/api/assets/${input.assetId}/reviews/${input.reviewId}/translate`,
364
+ // The translate endpoint expects the review content in the body.
365
+ {
366
+ rating: input.rating,
367
+ title: input.title,
368
+ review: input.body,
369
+ storeFront: input.storeFront,
370
+ appVersionString: input.appVersionString,
371
+ lastModified: Date.now()
372
+ }
373
+ );
312
374
  return {
313
375
  content: [
314
376
  {
@@ -388,9 +450,11 @@ var DIMENSION_INFO = {
388
450
  };
389
451
  async function discoverMetrics(input, client) {
390
452
  try {
391
- const response = await client.get(`/api/assets/${input.assetId}/metrics/availability`);
392
- const availableMetrics = response.data?.availableMetrics || [];
393
- const metricGroups = response.data?.metricGroups || {};
453
+ const response = await client.get(
454
+ `/api/assets/${input.assetId}/metrics/availability`
455
+ );
456
+ const availableMetrics = response.data?.availableMetrics ?? [];
457
+ const metricGroups = response.data?.metricGroups ?? {};
394
458
  const enriched = availableMetrics.map((metricName) => {
395
459
  const info = METRIC_INFO[metricName];
396
460
  return {
@@ -441,10 +505,13 @@ async function getMetrics(input, client) {
441
505
  if (input.dimensionFilter && input.dimension) {
442
506
  params[`filter.${input.dimension}`] = input.dimensionFilter;
443
507
  }
444
- const response = await client.get(`/api/assets/${input.assetId}/metrics/timeseries`, params);
445
- const data = response.data || {};
446
- const changes = response.changes || {};
447
- const meta = response.meta || {};
508
+ const response = await client.get(
509
+ `/api/assets/${input.assetId}/metrics/timeseries`,
510
+ params
511
+ );
512
+ const data = response.data ?? {};
513
+ const changes = response.changes ?? {};
514
+ const meta = response.meta ?? {};
448
515
  const summaries = {};
449
516
  for (const [metricName, timeseries] of Object.entries(data)) {
450
517
  const points = timeseries;
@@ -500,17 +567,20 @@ async function discoverDimensions(input, client) {
500
567
  assetId: input.assetId,
501
568
  dimension: input.dimension,
502
569
  displayName: DIMENSION_INFO[input.dimension] || input.dimension,
503
- values: response2.data || response2
570
+ values: response2.data ?? response2
504
571
  })
505
572
  }]
506
573
  };
507
574
  }
508
575
  const response = await client.get(`/api/assets/${input.assetId}/metrics/dimensions`);
509
- const dimensions = response.data || response || [];
510
- const enriched = (Array.isArray(dimensions) ? dimensions : Object.keys(dimensions)).map((dim) => ({
511
- name: dim,
512
- displayName: DIMENSION_INFO[dim] || dim
513
- }));
576
+ const dimensions = Array.isArray(response.data) ? response.data : [];
577
+ const enriched = dimensions.map((dim) => {
578
+ if (typeof dim === "string") {
579
+ return { name: dim, displayName: DIMENSION_INFO[dim] || dim };
580
+ }
581
+ const id = dim.id ?? "";
582
+ return { name: id, displayName: dim.label ?? DIMENSION_INFO[id] ?? id };
583
+ });
514
584
  return {
515
585
  content: [{
516
586
  type: "text",
@@ -528,6 +598,9 @@ async function discoverDimensions(input, client) {
528
598
 
529
599
  // src/tools/agents.ts
530
600
  import { z as z4 } from "zod";
601
+ function asRecord(value) {
602
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
603
+ }
531
604
  var AGENT_TYPES = [
532
605
  "review",
533
606
  "monitoring",
@@ -576,7 +649,7 @@ async function listAgents(_input, client) {
576
649
  let apiAgents = null;
577
650
  try {
578
651
  const response = await client.get("/api/agents");
579
- apiAgents = response.data || response.agents || response;
652
+ apiAgents = Array.isArray(response) ? response : response.data ?? response.agents ?? null;
580
653
  } catch {
581
654
  }
582
655
  if (apiAgents && Array.isArray(apiAgents)) {
@@ -626,21 +699,26 @@ async function getAgentDetails(input, client) {
626
699
  const params = {};
627
700
  if (input.assetId)
628
701
  params.assetId = input.assetId;
629
- const response2 = await client.get(`/api/agents/${input.agentType}`, params);
630
- return {
631
- content: [
632
- {
633
- type: "text",
634
- text: formatAsJson(response2.data || response2)
635
- }
636
- ]
637
- };
702
+ const response2 = await client.get(`/api/agents/by-type/${input.agentType}`, params);
703
+ if (response2.agent) {
704
+ return {
705
+ content: [
706
+ {
707
+ type: "text",
708
+ text: formatAsJson({
709
+ agent: response2.agent,
710
+ recentRuns: response2.recentRuns ?? []
711
+ })
712
+ }
713
+ ]
714
+ };
715
+ }
638
716
  } catch {
639
717
  }
640
718
  const response = {
641
719
  type: input.agentType,
642
720
  description: AGENT_DESCRIPTIONS[input.agentType],
643
- message: "Detailed agent configuration is available through the platform dashboard."
721
+ message: `No ${input.agentType} agent is provisioned for this organization yet. Activate it from the Agents page or with trigger_agent_run once created.`
644
722
  };
645
723
  return {
646
724
  content: [
@@ -664,33 +742,44 @@ async function getAgentDetails(input, client) {
664
742
  }
665
743
  async function getAgentRunHistory(input, client) {
666
744
  try {
745
+ let agentId;
667
746
  try {
668
- const params = {
669
- limit: input.limit
670
- };
747
+ const byTypeParams = {};
671
748
  if (input.assetId)
672
- params.assetId = input.assetId;
673
- const response = await client.get(`/api/agents/${input.agentType}/runs`, params);
749
+ byTypeParams.assetId = input.assetId;
750
+ const byType = await client.get(
751
+ `/api/agents/by-type/${input.agentType}`,
752
+ byTypeParams
753
+ );
754
+ agentId = byType.agent?.id;
755
+ } catch {
756
+ }
757
+ if (!agentId) {
674
758
  return {
675
759
  content: [
676
760
  {
677
761
  type: "text",
678
762
  text: formatAsJson({
679
763
  agentType: input.agentType,
680
- ...response.data || response
764
+ runs: [],
765
+ message: `No ${input.agentType} agent is provisioned for this organization yet, so there is no run history.`
681
766
  })
682
767
  }
683
768
  ]
684
769
  };
685
- } catch {
686
770
  }
771
+ const response = await client.get(
772
+ `/api/agents/${agentId}/runs`,
773
+ { limit: input.limit }
774
+ );
687
775
  return {
688
776
  content: [
689
777
  {
690
778
  type: "text",
691
779
  text: formatAsJson({
692
780
  agentType: input.agentType,
693
- message: `Run history for ${input.agentType} agent is available through the platform dashboard. This agent type does not have a dedicated run log table yet.`
781
+ agentId,
782
+ ...asRecord(response.data ?? response)
694
783
  })
695
784
  }
696
785
  ]
@@ -709,15 +798,16 @@ async function getAgentRunHistory(input, client) {
709
798
  }
710
799
  async function triggerAgentRun(input, client) {
711
800
  try {
712
- const body = {};
713
- if (input.assetId)
714
- body.assetId = input.assetId;
715
- const response = await client.post(`/api/agents/${input.agentId}/run`, body);
801
+ const body = input.assetId ? { triggerContext: { assetId: input.assetId } } : {};
802
+ const response = await client.post(
803
+ `/api/agents/${input.agentId}/run`,
804
+ body
805
+ );
716
806
  return {
717
807
  content: [
718
808
  {
719
809
  type: "text",
720
- text: formatAsJson(response.data || response)
810
+ text: formatAsJson(response.data ?? response)
721
811
  }
722
812
  ]
723
813
  };
@@ -735,12 +825,14 @@ async function triggerAgentRun(input, client) {
735
825
  }
736
826
  async function pauseAgent(input, client) {
737
827
  try {
738
- const response = await client.post(`/api/agents/${input.agentId}/pause`);
828
+ const response = await client.post(
829
+ `/api/agents/${input.agentId}/pause`
830
+ );
739
831
  return {
740
832
  content: [
741
833
  {
742
834
  type: "text",
743
- text: formatAsJson(response.data || response)
835
+ text: formatAsJson(response.data ?? response)
744
836
  }
745
837
  ]
746
838
  };
@@ -758,12 +850,14 @@ async function pauseAgent(input, client) {
758
850
  }
759
851
  async function resumeAgent(input, client) {
760
852
  try {
761
- const response = await client.post(`/api/agents/${input.agentId}/resume`);
853
+ const response = await client.post(
854
+ `/api/agents/${input.agentId}/resume`
855
+ );
762
856
  return {
763
857
  content: [
764
858
  {
765
859
  type: "text",
766
- text: formatAsJson(response.data || response)
860
+ text: formatAsJson(response.data ?? response)
767
861
  }
768
862
  ]
769
863
  };
@@ -781,12 +875,14 @@ async function resumeAgent(input, client) {
781
875
  }
782
876
  async function getAgentActivity(input, client) {
783
877
  try {
784
- const response = await client.get(`/api/agents/${input.agentId}/activity`);
878
+ const response = await client.get(
879
+ `/api/agents/${input.agentId}/activity`
880
+ );
785
881
  return {
786
882
  content: [
787
883
  {
788
884
  type: "text",
789
- text: formatAsJson(response.data || response)
885
+ text: formatAsJson(response.data ?? response)
790
886
  }
791
887
  ]
792
888
  };
@@ -844,34 +940,40 @@ async function getAnomalies(input, client) {
844
940
  params.fromDate = input.fromDate;
845
941
  if (input.toDate)
846
942
  params.toDate = input.toDate;
847
- if (input.excludeDismissed)
848
- params.excludeDismissed = input.excludeDismissed;
849
- const response = await client.get("/api/anomalies", params);
850
- const anomalies = response.data || response.anomalies || [];
943
+ const response = await client.get(
944
+ "/api/anomalies",
945
+ params
946
+ );
947
+ let anomalies = response.data ?? response.anomalies ?? [];
948
+ if (input.excludeDismissed && !input.status) {
949
+ anomalies = anomalies.filter((a) => a.status !== "dismissed");
950
+ }
851
951
  return {
852
952
  content: [
853
953
  {
854
954
  type: "text",
855
955
  text: formatAsJson({
856
956
  total: anomalies.length,
857
- anomalies: anomalies.map((a) => ({
858
- id: a.id,
859
- assetId: a.assetId,
860
- assetName: a.assetName,
861
- assetIcon: a.assetIcon,
862
- anomalyDate: a.anomalyDate,
863
- metricName: a.metricName,
864
- sourceType: a.sourceType,
865
- type: a.type,
866
- severity: a.severity,
867
- actualValue: parseFloat(a.actualValue || "0"),
868
- expectedValue: parseFloat(a.expectedValue || "0"),
869
- deviationPercent: parseFloat(a.deviationPercent || "0"),
870
- confidence: parseFloat(a.confidence || "0"),
871
- explanation: a.explanation,
872
- suggestedActions: a.suggestedActions,
873
- status: a.status,
874
- detectedAt: a.detectedAt
957
+ anomalies: anomalies.map((anomaly) => ({
958
+ id: anomaly.id,
959
+ assetId: anomaly.assetId,
960
+ assetName: anomaly.assetName,
961
+ assetIcon: anomaly.assetIcon,
962
+ anomalyDate: anomaly.anomalyDate,
963
+ metricName: anomaly.metricName,
964
+ sourceType: anomaly.sourceType,
965
+ type: anomaly.type,
966
+ severity: anomaly.severity,
967
+ actualValue: parseFloat(String(anomaly.actualValue || "0")),
968
+ expectedValue: parseFloat(String(anomaly.expectedValue || "0")),
969
+ deviationPercent: parseFloat(
970
+ String(anomaly.deviationPercent || "0")
971
+ ),
972
+ confidence: parseFloat(String(anomaly.confidence || "0")),
973
+ explanation: anomaly.explanation,
974
+ suggestedActions: anomaly.suggestedActions,
975
+ status: anomaly.status,
976
+ detectedAt: anomaly.detectedAt
875
977
  }))
876
978
  })
877
979
  }
@@ -970,37 +1072,46 @@ var getAdsPerformanceSchema = z6.object({
970
1072
  });
971
1073
  async function getAdsPerformance(input, client) {
972
1074
  try {
973
- const params = {
974
- limit: input.limit
975
- };
1075
+ const params = {};
976
1076
  if (input.assetId)
977
1077
  params.assetId = input.assetId;
978
- if (input.platform)
979
- params.platform = input.platform;
980
1078
  if (input.fromDate)
981
- params.fromDate = input.fromDate;
1079
+ params.from = input.fromDate;
982
1080
  if (input.toDate)
983
- params.toDate = input.toDate;
984
- const response = await client.get("/api/ads/campaigns", params);
985
- const campaigns = response.data || response.campaigns || [];
1081
+ params.to = input.toDate;
1082
+ const response = await client.get(
1083
+ "/api/ads-agent/campaigns",
1084
+ params
1085
+ );
1086
+ let campaigns = response.data ?? response.campaigns ?? [];
1087
+ if (input.platform) {
1088
+ campaigns = campaigns.filter((c) => c.platform === input.platform);
1089
+ }
1090
+ campaigns = campaigns.slice(0, input.limit);
986
1091
  return {
987
1092
  content: [
988
1093
  {
989
1094
  type: "text",
990
1095
  text: formatAsJson({
991
1096
  total: campaigns.length,
992
- campaigns: campaigns.map((c) => ({
993
- id: c.id,
994
- assetId: c.assetId,
995
- assetName: c.assetName,
996
- platformCampaignId: c.platformCampaignId,
997
- name: c.name,
998
- status: c.status,
999
- objective: c.objective,
1000
- platform: c.platform,
1001
- linkSource: c.linkSource,
1002
- linkedAt: c.linkedAt,
1003
- recentPerformance: c.recentPerformance || null
1097
+ campaigns: campaigns.map((campaign) => ({
1098
+ id: campaign.id,
1099
+ assetId: campaign.assetId,
1100
+ assetName: campaign.assetName,
1101
+ platformCampaignId: campaign.platformCampaignId,
1102
+ name: campaign.name,
1103
+ status: campaign.status,
1104
+ objective: campaign.objective,
1105
+ platform: campaign.platform,
1106
+ linkSource: campaign.linkSource,
1107
+ linkedAt: campaign.linkedAt,
1108
+ totalSpend: campaign.totalSpend ?? null,
1109
+ totalImpressions: campaign.totalImpressions ?? null,
1110
+ totalTaps: campaign.totalTaps ?? null,
1111
+ totalInstalls: campaign.totalInstalls ?? null,
1112
+ avgCPI: campaign.avgCPI ?? null,
1113
+ avgTTR: campaign.avgTTR ?? null,
1114
+ avgCVR: campaign.avgCVR ?? null
1004
1115
  }))
1005
1116
  })
1006
1117
  }
@@ -1078,13 +1189,17 @@ async function getGrowthScore(input, client) {
1078
1189
  import { z as z8 } from "zod";
1079
1190
  var getForecastsSchema = z8.object({
1080
1191
  assetId: z8.string().uuid().describe("App UUID to get forecasts for"),
1081
- dataPoints: z8.number().int().min(4).max(52).default(12).describe("Number of historical data points to include")
1192
+ metric: z8.enum(["proceeds", "total_revenue", "total_downloads", "active_subs", "new_trials"]).describe("The metric to forecast"),
1193
+ forecastWeeks: z8.number().int().min(4).max(156).optional().describe("Weeks to forecast forward (default 104)"),
1194
+ historicalWeeks: z8.number().int().min(12).max(156).optional().describe("Weeks of history to base the forecast on (default 104)")
1082
1195
  });
1083
1196
  async function getForecasts(input, client) {
1084
1197
  try {
1085
1198
  const response = await client.get("/api/forecasting/forecast", {
1086
1199
  assetId: input.assetId,
1087
- dataPoints: input.dataPoints
1200
+ metricName: input.metric,
1201
+ forecastWeeks: input.forecastWeeks,
1202
+ historicalWeeks: input.historicalWeeks
1088
1203
  });
1089
1204
  return {
1090
1205
  content: [
@@ -1109,18 +1224,34 @@ async function getForecasts(input, client) {
1109
1224
 
1110
1225
  // src/tools/dashboard.ts
1111
1226
  import { z as z9 } from "zod";
1227
+ function parseDashboardNumber(value) {
1228
+ if (value === null || value === void 0 || value === "")
1229
+ return null;
1230
+ const parsed = typeof value === "number" ? value : Number.parseFloat(value);
1231
+ return Number.isFinite(parsed) ? parsed : null;
1232
+ }
1233
+ function getSidebarAssets(response) {
1234
+ if (!response)
1235
+ return [];
1236
+ if (Array.isArray(response))
1237
+ return response;
1238
+ return response.data ?? [];
1239
+ }
1112
1240
  var getDashboardOverviewSchema = z9.object({});
1113
1241
  async function getDashboardOverview(_input, client) {
1114
1242
  try {
1243
+ const overviewPromise = client.get("/api/dashboard/overview-metrics").catch(() => null);
1244
+ const sidebarPromise = client.get(
1245
+ "/api/dashboard/sidebar-assets"
1246
+ ).catch(() => null);
1115
1247
  const [overviewResponse, sidebarResponse] = await Promise.all([
1116
- client.get("/api/dashboard/overview-metrics").catch(() => null),
1117
- client.get("/api/dashboard/sidebar-assets").catch(() => null)
1248
+ overviewPromise,
1249
+ sidebarPromise
1118
1250
  ]);
1119
- const overview = overviewResponse?.data || overviewResponse || {};
1120
- const sidebarAssets = sidebarResponse?.data || sidebarResponse || [];
1121
- const apps = Array.isArray(sidebarAssets) ? sidebarAssets : [];
1122
- const totalValuation = apps.reduce((sum, a) => {
1123
- return sum + parseFloat(a.currentValuation || "0");
1251
+ const overview = overviewResponse?.data ?? overviewResponse ?? {};
1252
+ const apps = getSidebarAssets(sidebarResponse);
1253
+ const totalValuation = apps.reduce((sum, app) => {
1254
+ return sum + (parseDashboardNumber(app.currentValuation) ?? 0);
1124
1255
  }, 0);
1125
1256
  return {
1126
1257
  content: [
@@ -1130,16 +1261,16 @@ async function getDashboardOverview(_input, client) {
1130
1261
  portfolio: {
1131
1262
  totalApps: apps.length,
1132
1263
  totalValuation: totalValuation > 0 ? totalValuation : null,
1133
- apps: apps.map((a) => ({
1134
- id: a.id,
1135
- name: a.name,
1136
- bundleId: a.bundleId,
1137
- platform: a.appleAppId ? "ios" : a.googleAppId ? "android" : "unknown",
1138
- currentValuation: a.currentValuation ? parseFloat(a.currentValuation) : null,
1139
- rating: a.rating ? parseFloat(a.rating) : null,
1140
- ratingCount: a.ratingCount || null,
1141
- category: a.category || null,
1142
- iconUrl: a.iconUrl || null
1264
+ apps: apps.map((app) => ({
1265
+ id: app.id,
1266
+ name: app.name,
1267
+ bundleId: app.bundleId,
1268
+ platform: app.appleAppId ? "ios" : app.googleAppId ? "android" : "unknown",
1269
+ currentValuation: parseDashboardNumber(app.currentValuation),
1270
+ rating: parseDashboardNumber(app.rating),
1271
+ ratingCount: app.ratingCount ?? null,
1272
+ category: app.category ?? null,
1273
+ iconUrl: app.iconUrl ?? null
1143
1274
  }))
1144
1275
  },
1145
1276
  overview
@@ -1162,13 +1293,15 @@ async function getDashboardOverview(_input, client) {
1162
1293
 
1163
1294
  // src/tools/actions.ts
1164
1295
  import { z as z10 } from "zod";
1296
+ function asRecord2(value) {
1297
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1298
+ }
1165
1299
  var listPendingActionsSchema = z10.object({
1166
1300
  assetId: z10.string().uuid().optional().describe("Filter by app UUID"),
1167
1301
  limit: z10.number().int().min(1).max(100).default(50).describe("Maximum number of actions to return")
1168
1302
  });
1169
1303
  var approveActionSchema = z10.object({
1170
- actionId: z10.string().describe("The draft reply ID to approve"),
1171
- editedReply: z10.string().optional().describe("Optionally modify the reply text before approving")
1304
+ actionId: z10.string().describe("The pending action ID to approve")
1172
1305
  });
1173
1306
  var rejectActionSchema = z10.object({
1174
1307
  actionId: z10.string().describe("The draft reply ID to reject/delete")
@@ -1178,10 +1311,16 @@ async function listPendingActions(input, client) {
1178
1311
  const params = {
1179
1312
  limit: input.limit
1180
1313
  };
1181
- if (input.assetId)
1182
- params.assetId = input.assetId;
1183
- const response = await client.get("/api/pending-actions", params);
1184
- const actions = response.data || response.actions || [];
1314
+ const response = await client.get(
1315
+ "/api/pending-actions",
1316
+ params
1317
+ );
1318
+ let actions = response.data ?? response.actions ?? [];
1319
+ if (input.assetId) {
1320
+ actions = actions.filter(
1321
+ (action) => action.assetId === input.assetId
1322
+ );
1323
+ }
1185
1324
  return {
1186
1325
  content: [
1187
1326
  {
@@ -1207,10 +1346,10 @@ async function listPendingActions(input, client) {
1207
1346
  }
1208
1347
  async function approveAction(input, client) {
1209
1348
  try {
1210
- const body = {};
1211
- if (input.editedReply)
1212
- body.editedReply = input.editedReply;
1213
- const response = await client.post(`/api/pending-actions/${input.actionId}/approve`, body);
1349
+ const response = await client.post(
1350
+ `/api/pending-actions/${input.actionId}/approve`,
1351
+ {}
1352
+ );
1214
1353
  return {
1215
1354
  content: [
1216
1355
  {
@@ -1220,7 +1359,7 @@ async function approveAction(input, client) {
1220
1359
  actionId: input.actionId,
1221
1360
  status: "approved",
1222
1361
  message: "Reply has been approved and queued for sending",
1223
- ...response.data || {}
1362
+ ...asRecord2(response.data)
1224
1363
  })
1225
1364
  }
1226
1365
  ]
@@ -1239,7 +1378,9 @@ async function approveAction(input, client) {
1239
1378
  }
1240
1379
  async function rejectAction(input, client) {
1241
1380
  try {
1242
- const response = await client.post(`/api/pending-actions/${input.actionId}/reject`);
1381
+ const response = await client.post(
1382
+ `/api/pending-actions/${input.actionId}/reject`
1383
+ );
1243
1384
  return {
1244
1385
  content: [
1245
1386
  {
@@ -1249,7 +1390,7 @@ async function rejectAction(input, client) {
1249
1390
  actionId: input.actionId,
1250
1391
  status: "rejected",
1251
1392
  message: "Draft reply has been rejected and removed",
1252
- ...response.data || {}
1393
+ ...asRecord2(response.data)
1253
1394
  })
1254
1395
  }
1255
1396
  ]
@@ -1283,10 +1424,13 @@ var getAsoExperimentsSchema = z11.object({
1283
1424
  assetId: z11.string().uuid().describe("App UUID to list ASO experiments for"),
1284
1425
  status: z11.enum(["proposed", "approved", "applied", "measuring", "completed", "reverted"]).optional().describe("Filter experiments by status"),
1285
1426
  limit: z11.number().int().min(1).max(100).default(20).describe("Maximum number of experiments to return"),
1286
- offset: z11.number().int().min(0).default(0).describe("Number of experiments to skip for pagination")
1427
+ cursor: z11.string().optional().describe("Pagination cursor from a previous response (meta.nextCursor)")
1287
1428
  });
1288
1429
  var getAsoLocaleSnapshotsSchema = z11.object({
1289
- assetId: z11.string().uuid().describe("App UUID to get locale snapshots for")
1430
+ assetId: z11.string().uuid().describe("App UUID to get locale snapshots for"),
1431
+ store: z11.enum(["ios", "android"]).default("ios").describe("Which store listing to read"),
1432
+ locale: z11.string().optional().describe('Filter to a single locale (e.g. "en-US"). Returns all locales if omitted.'),
1433
+ limit: z11.number().int().min(1).max(500).optional().describe("Maximum snapshot rows to return (newest first)")
1290
1434
  });
1291
1435
  var triggerAsoAnalysisSchema = z11.object({
1292
1436
  assetId: z11.string().uuid().describe("App UUID to trigger ASO analysis for")
@@ -1367,7 +1511,7 @@ async function getAsoExperiments(input, client) {
1367
1511
  try {
1368
1512
  const params = {
1369
1513
  limit: input.limit,
1370
- offset: input.offset
1514
+ cursor: input.cursor
1371
1515
  };
1372
1516
  if (input.status)
1373
1517
  params.status = input.status;
@@ -1394,7 +1538,14 @@ async function getAsoExperiments(input, client) {
1394
1538
  }
1395
1539
  async function getAsoLocaleSnapshots(input, client) {
1396
1540
  try {
1397
- const response = await client.get(`/api/assets/${input.assetId}/aso/locale-snapshots`);
1541
+ const response = await client.get(
1542
+ `/api/assets/${input.assetId}/aso/listing-snapshots`,
1543
+ {
1544
+ store: input.store,
1545
+ locale: input.locale,
1546
+ limit: input.limit
1547
+ }
1548
+ );
1398
1549
  return {
1399
1550
  content: [
1400
1551
  {
@@ -1417,7 +1568,10 @@ async function getAsoLocaleSnapshots(input, client) {
1417
1568
  }
1418
1569
  async function triggerAsoAnalysis(input, client) {
1419
1570
  try {
1420
- const response = await client.post(`/api/assets/${input.assetId}/aso/analyze`);
1571
+ const response = await client.post(
1572
+ `/api/assets/${input.assetId}/aso/refetch`,
1573
+ { force: true }
1574
+ );
1421
1575
  return {
1422
1576
  content: [
1423
1577
  {
@@ -1440,6 +1594,7 @@ async function triggerAsoAnalysis(input, client) {
1440
1594
  }
1441
1595
 
1442
1596
  // src/tools/chat.ts
1597
+ import { randomUUID } from "node:crypto";
1443
1598
  import { z as z12 } from "zod";
1444
1599
  var listConversationsSchema = z12.object({
1445
1600
  limit: z12.number().int().min(1).max(100).default(20).describe("Maximum number of conversations to return")
@@ -1509,23 +1664,81 @@ async function getConversationMessages(input, client) {
1509
1664
  };
1510
1665
  }
1511
1666
  }
1667
+ function parseUiMessageStream(raw) {
1668
+ let reply = "";
1669
+ const toolsUsed = [];
1670
+ for (const line of raw.split("\n")) {
1671
+ if (!line.startsWith("data:"))
1672
+ continue;
1673
+ const payload = line.slice(5).trim();
1674
+ if (!payload || payload === "[DONE]")
1675
+ continue;
1676
+ try {
1677
+ const event = JSON.parse(payload);
1678
+ if (event.type === "text-delta") {
1679
+ reply += String(event.delta ?? event.textDelta ?? "");
1680
+ } else if (typeof event.type === "string" && event.type === "tool-input-start" && typeof event.toolName === "string") {
1681
+ toolsUsed.push(event.toolName);
1682
+ }
1683
+ } catch {
1684
+ }
1685
+ }
1686
+ return { reply, toolsUsed };
1687
+ }
1512
1688
  async function sendChatMessage(input, client) {
1513
1689
  try {
1690
+ const conversationId = input.conversationId ?? randomUUID();
1691
+ let priorMessages = [];
1692
+ if (input.conversationId) {
1693
+ const history = await client.get(`/api/chat/messages/${input.conversationId}`, { limit: 1e3 });
1694
+ for (const message of history.data ?? []) {
1695
+ const textParts = (message.parts ?? []).filter(
1696
+ (part) => part.type === "text" && typeof part.text === "string"
1697
+ );
1698
+ if (textParts.length === 0)
1699
+ continue;
1700
+ priorMessages.push({
1701
+ id: message.id,
1702
+ role: message.role,
1703
+ parts: textParts
1704
+ });
1705
+ }
1706
+ priorMessages = priorMessages.slice(-100);
1707
+ }
1514
1708
  const body = {
1515
- message: input.message
1709
+ id: conversationId,
1710
+ messages: [
1711
+ ...priorMessages,
1712
+ {
1713
+ id: randomUUID(),
1714
+ role: "user",
1715
+ parts: [{ type: "text", text: input.message }]
1716
+ }
1717
+ ]
1516
1718
  };
1517
- if (input.conversationId)
1518
- body.conversationId = input.conversationId;
1519
- if (input.agentType)
1520
- body.agentType = input.agentType;
1521
- const response = await client.post("/api/chat", body);
1719
+ if (input.agentType) {
1720
+ body.agentContext = {
1721
+ agentId: null,
1722
+ agentType: input.agentType,
1723
+ agentName: input.agentType,
1724
+ surface: "mcp"
1725
+ };
1726
+ }
1727
+ const raw = await client.postStream("/api/chat", body);
1728
+ const { reply, toolsUsed } = parseUiMessageStream(raw);
1522
1729
  return {
1523
1730
  content: [
1524
1731
  {
1525
1732
  type: "text",
1526
- text: formatAsJson(response.data || response)
1733
+ text: formatAsJson({
1734
+ conversationId,
1735
+ reply: reply || "The assistant returned no text response.",
1736
+ toolsUsed,
1737
+ tip: "Pass this conversationId to continue the conversation or to get_conversation_messages for the full transcript."
1738
+ })
1527
1739
  }
1528
- ]
1740
+ ],
1741
+ ...reply ? {} : { isError: true }
1529
1742
  };
1530
1743
  } catch (error) {
1531
1744
  return {
@@ -1543,8 +1756,8 @@ async function sendChatMessage(input, client) {
1543
1756
  // src/tools/index.ts
1544
1757
  function registerTools(server, client, options = {}) {
1545
1758
  const rateLimiter = options.rateLimiter ?? new RateLimiter(100, 6e4);
1546
- function wrapTool(_toolName, handler) {
1547
- return async (input) => {
1759
+ function wrapTool(handler) {
1760
+ const callback = async (input) => {
1548
1761
  const rateCheck = rateLimiter.check("default");
1549
1762
  if (!rateCheck.allowed) {
1550
1763
  return {
@@ -1561,6 +1774,7 @@ function registerTools(server, client, options = {}) {
1561
1774
  }
1562
1775
  return handler(input, client);
1563
1776
  };
1777
+ return callback;
1564
1778
  }
1565
1779
  function tool(name, title, description, schema, handler, annotations) {
1566
1780
  server.registerTool(
@@ -1571,8 +1785,7 @@ function registerTools(server, client, options = {}) {
1571
1785
  inputSchema: schema.shape,
1572
1786
  annotations: { title, ...annotations }
1573
1787
  },
1574
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1575
- wrapTool(name, handler)
1788
+ wrapTool(handler)
1576
1789
  );
1577
1790
  }
1578
1791
  tool(
@@ -1618,7 +1831,7 @@ function registerTools(server, client, options = {}) {
1618
1831
  tool(
1619
1832
  "translate_review",
1620
1833
  "Translate review",
1621
- "Translate a review to English. Useful for reviews written in other languages. Returns the translated text. Does not modify the review in Fload.",
1834
+ "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.",
1622
1835
  translateReviewSchema,
1623
1836
  translateReview,
1624
1837
  { readOnlyHint: true, openWorldHint: true }
@@ -1762,7 +1975,7 @@ function registerTools(server, client, options = {}) {
1762
1975
  tool(
1763
1976
  "get_forecasts",
1764
1977
  "Get forecasts",
1765
- "Get valuation-based forecasts and trend analysis for an app. Returns historical valuation data points, trend statistics (direction, volatility), and simple linear projections. For detailed metric forecasting with statistical models, use the platform dashboard.",
1978
+ "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.",
1766
1979
  getForecastsSchema,
1767
1980
  getForecasts,
1768
1981
  { readOnlyHint: true, openWorldHint: false }
@@ -1931,6 +2144,7 @@ export {
1931
2144
  listConversationsSchema,
1932
2145
  listPendingActions,
1933
2146
  listPendingActionsSchema,
2147
+ parseUiMessageStream,
1934
2148
  pauseAgent,
1935
2149
  pauseAgentSchema,
1936
2150
  registerTools,