@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/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", { limit: 100, offset: 0 });
141
- const match = (allApps.data || []).find((a) => a.bundleId === input.bundleId);
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(`/api/assets/${assetId}`);
156
- const app = response.data || response;
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", { limit: 100, offset: 0 });
220
- const match = (allApps.data || []).find((a) => a.bundleId === input.bundleId);
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("/api/reviews", params);
251
- const reviews = response.data || response.reviews || [];
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, r) => sum + (r.rating || 0), 0) / reviews.length).toFixed(2) : 0,
255
- repliedCount: reviews.filter((r) => r.developerResponse || r.hasReply).length,
256
- unrepliedCount: reviews.filter((r) => !r.developerResponse && !r.hasReply).length
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((r) => ({
265
- id: r.id,
266
- appId: r.appId,
267
- platform: r.platform,
268
- rating: r.rating,
269
- title: r.title,
270
- body: r.body,
271
- author: r.nickname || r.author,
272
- date: r.lastModified || r.date,
273
- version: r.appVersionString || r.version,
274
- storeFront: r.storeFront,
275
- hasReply: !!(r.developerResponse || r.hasReply),
276
- reply: r.developerResponse ? typeof r.developerResponse === "object" ? r.developerResponse.response : r.developerResponse : r.reply || null
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(`/api/reviews/${input.reviewId}/generate-reply`, {
297
- assetId: input.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(`/api/reviews/${input.reviewId}/respond`, {
322
- assetId: input.assetId,
323
- response: input.response
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(`/api/reviews/${input.reviewId}/translate`, {
348
- assetId: input.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(`/api/assets/${input.assetId}/metrics/availability`);
430
- const availableMetrics = response.data?.availableMetrics || [];
431
- const metricGroups = response.data?.metricGroups || {};
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(`/api/assets/${input.assetId}/metrics/timeseries`, params);
483
- const data = response.data || {};
484
- const changes = response.changes || {};
485
- const meta = response.meta || {};
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 || response2
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 || response || [];
548
- const enriched = (Array.isArray(dimensions) ? dimensions : Object.keys(dimensions)).map((dim) => ({
549
- name: dim,
550
- displayName: DIMENSION_INFO[dim] || dim
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 || response.agents || response;
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
- return {
669
- content: [
670
- {
671
- type: "text",
672
- text: formatAsJson(response2.data || response2)
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: "Detailed agent configuration is available through the platform dashboard."
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 params = {
707
- limit: input.limit
708
- };
785
+ const byTypeParams = {};
709
786
  if (input.assetId)
710
- params.assetId = input.assetId;
711
- const response = await client.get(`/api/agents/${input.agentType}/runs`, params);
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
- ...response.data || response
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
- 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.`
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
- if (input.assetId)
752
- body.assetId = input.assetId;
753
- const response = await client.post(`/api/agents/${input.agentId}/run`, body);
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 || response)
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(`/api/agents/${input.agentId}/pause`);
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 || response)
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(`/api/agents/${input.agentId}/resume`);
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 || response)
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(`/api/agents/${input.agentId}/activity`);
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 || response)
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
- if (input.excludeDismissed)
886
- params.excludeDismissed = input.excludeDismissed;
887
- const response = await client.get("/api/anomalies", params);
888
- const anomalies = response.data || response.anomalies || [];
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((a) => ({
896
- id: a.id,
897
- assetId: a.assetId,
898
- assetName: a.assetName,
899
- assetIcon: a.assetIcon,
900
- anomalyDate: a.anomalyDate,
901
- metricName: a.metricName,
902
- sourceType: a.sourceType,
903
- type: a.type,
904
- severity: a.severity,
905
- actualValue: parseFloat(a.actualValue || "0"),
906
- expectedValue: parseFloat(a.expectedValue || "0"),
907
- deviationPercent: parseFloat(a.deviationPercent || "0"),
908
- confidence: parseFloat(a.confidence || "0"),
909
- explanation: a.explanation,
910
- suggestedActions: a.suggestedActions,
911
- status: a.status,
912
- detectedAt: a.detectedAt
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.fromDate = input.fromDate;
1117
+ params.from = input.fromDate;
1020
1118
  if (input.toDate)
1021
- params.toDate = input.toDate;
1022
- const response = await client.get("/api/ads/campaigns", params);
1023
- const campaigns = response.data || response.campaigns || [];
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((c) => ({
1031
- id: c.id,
1032
- assetId: c.assetId,
1033
- assetName: c.assetName,
1034
- platformCampaignId: c.platformCampaignId,
1035
- name: c.name,
1036
- status: c.status,
1037
- objective: c.objective,
1038
- platform: c.platform,
1039
- linkSource: c.linkSource,
1040
- linkedAt: c.linkedAt,
1041
- recentPerformance: c.recentPerformance || null
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
- dataPoints: z8.number().int().min(4).max(52).default(12).describe("Number of historical data points to include")
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
- dataPoints: input.dataPoints
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
- client.get("/api/dashboard/overview-metrics").catch(() => null),
1155
- client.get("/api/dashboard/sidebar-assets").catch(() => null)
1286
+ overviewPromise,
1287
+ sidebarPromise
1156
1288
  ]);
1157
- const overview = overviewResponse?.data || overviewResponse || {};
1158
- const sidebarAssets = sidebarResponse?.data || sidebarResponse || [];
1159
- const apps = Array.isArray(sidebarAssets) ? sidebarAssets : [];
1160
- const totalValuation = apps.reduce((sum, a) => {
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((a) => ({
1172
- id: a.id,
1173
- name: a.name,
1174
- bundleId: a.bundleId,
1175
- platform: a.appleAppId ? "ios" : a.googleAppId ? "android" : "unknown",
1176
- currentValuation: a.currentValuation ? parseFloat(a.currentValuation) : null,
1177
- rating: a.rating ? parseFloat(a.rating) : null,
1178
- ratingCount: a.ratingCount || null,
1179
- category: a.category || null,
1180
- iconUrl: a.iconUrl || null
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 draft reply ID to approve"),
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
- if (input.assetId)
1220
- params.assetId = input.assetId;
1221
- const response = await client.get("/api/pending-actions", params);
1222
- const actions = response.data || response.actions || [];
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 body = {};
1249
- if (input.editedReply)
1250
- body.editedReply = input.editedReply;
1251
- const response = await client.post(`/api/pending-actions/${input.actionId}/approve`, body);
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(`/api/pending-actions/${input.actionId}/reject`);
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
- offset: z11.number().int().min(0).default(0).describe("Number of experiments to skip for pagination")
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
- offset: input.offset
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(`/api/assets/${input.assetId}/aso/locale-snapshots`);
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(`/api/assets/${input.assetId}/aso/analyze`);
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
- message: input.message
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.conversationId)
1556
- body.conversationId = input.conversationId;
1557
- if (input.agentType)
1558
- body.agentType = input.agentType;
1559
- const response = await client.post("/api/chat", body);
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(response.data || response)
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(_toolName, handler) {
1585
- return async (input) => {
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
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
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 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.",
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
- throw new Error(error.message || `API error: ${response.status}`);
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