@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/index.js CHANGED
@@ -1,3 +1,26 @@
1
+ // src/public-errors.ts
2
+ var INTERNAL_ERROR_PATTERNS = [
3
+ /\b(stack|trace|at\s+[\w.$<>]+\s*\()/i,
4
+ /\b(sql|postgres|drizzle|database|constraint|duplicate key|violates|relation)\b/i,
5
+ /\b(fetch|undici|socket|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|HTTP\s+\d{3})\b/i,
6
+ /\b(token|secret|api[_-]?key|authorization|bearer|cookie|password)\b/i,
7
+ /\b[a-z0-9]+_(secret|token|key)\b/i,
8
+ /\b(secret|token|key)[_-][a-z0-9]+\b/i,
9
+ /\b(scrap|browser|patchright|playwright|chromium|page\.|locator|selector)\b/i,
10
+ /\b(redis|bullmq|queue|job id|worker)\b/i
11
+ ];
12
+ function looksInternal(message) {
13
+ return INTERNAL_ERROR_PATTERNS.some((pattern) => pattern.test(message));
14
+ }
15
+ function publicMcpErrorMessage(message, fallbackMessage = "Fload could not complete the request. Please try again.") {
16
+ const raw = message instanceof Error ? message.message : typeof message === "string" ? message : "";
17
+ const normalized = raw.replace(/\s+/g, " ").trim();
18
+ if (!normalized || normalized.length > 180 || looksInternal(normalized)) {
19
+ return fallbackMessage;
20
+ }
21
+ return normalized;
22
+ }
23
+
1
24
  // src/api-client.ts
2
25
  var FloadApiClient = class {
3
26
  constructor(baseUrl, apiKey) {
@@ -6,17 +29,22 @@ var FloadApiClient = class {
6
29
  }
7
30
  async request(path, options = {}) {
8
31
  const url = `${this.baseUrl}${path}`;
32
+ const headers = {
33
+ Authorization: `Bearer ${this.apiKey}`,
34
+ ...options.body ? { "Content-Type": "application/json" } : {},
35
+ ...options.headers
36
+ };
9
37
  const response = await fetch(url, {
10
38
  ...options,
11
- headers: {
12
- "Authorization": `Bearer ${this.apiKey}`,
13
- "Content-Type": "application/json",
14
- ...options.headers
15
- }
39
+ headers
16
40
  });
17
41
  if (!response.ok) {
18
42
  const error = await response.json().catch(() => ({ message: response.statusText }));
19
- throw new Error(error.message || `API error: ${response.status}`);
43
+ const safeError = publicMcpErrorMessage(
44
+ error.publicMessage || error.message || response.statusText,
45
+ response.status >= 500 ? "Fload could not complete the request. Please try again." : "Fload rejected the request. Please check access and try again."
46
+ );
47
+ throw new Error(safeError);
20
48
  }
21
49
  return response.json();
22
50
  }
@@ -46,6 +74,31 @@ var FloadApiClient = class {
46
74
  async delete(path) {
47
75
  return this.request(path, { method: "DELETE" });
48
76
  }
77
+ /**
78
+ * POST to a streaming endpoint (SSE / AI SDK UI-message stream) and
79
+ * return the raw response body as text. Callers parse the event lines.
80
+ */
81
+ async postStream(path, body) {
82
+ const url = `${this.baseUrl}${path}`;
83
+ const response = await fetch(url, {
84
+ method: "POST",
85
+ headers: {
86
+ Authorization: `Bearer ${this.apiKey}`,
87
+ "Content-Type": "application/json",
88
+ Accept: "text/event-stream"
89
+ },
90
+ body: body ? JSON.stringify(body) : void 0
91
+ });
92
+ if (!response.ok) {
93
+ const error = await response.json().catch(() => ({ message: response.statusText }));
94
+ const safeError = publicMcpErrorMessage(
95
+ error.publicMessage || error.message || response.statusText,
96
+ response.status >= 500 ? "Fload could not complete the request. Please try again." : "Fload rejected the request. Please check access and try again."
97
+ );
98
+ throw new Error(safeError);
99
+ }
100
+ return response.text();
101
+ }
49
102
  };
50
103
 
51
104
  // src/server.ts
@@ -106,9 +159,12 @@ async function listApps(input, client) {
106
159
  try {
107
160
  const response = await client.get("/api/assets", {
108
161
  limit: input.limit,
109
- offset: 0
162
+ offset: 0,
163
+ // The listing hides assets without a valuation by default; "list all
164
+ // apps" must include apps that haven't been valued yet (FLO-734).
165
+ includeUnvalued: "true"
110
166
  });
111
- const apps = response.data || [];
167
+ const apps = response.data ?? [];
112
168
  const filtered = input.platform ? apps.filter((app) => {
113
169
  if (input.platform === "ios")
114
170
  return !!app.appleAppId;
@@ -152,8 +208,14 @@ async function getAppDetails(input, client) {
152
208
  }
153
209
  let assetId = input.assetId;
154
210
  if (!assetId && input.bundleId) {
155
- const allApps = await client.get("/api/assets", { limit: 100, offset: 0 });
156
- const match = (allApps.data || []).find((a) => a.bundleId === input.bundleId);
211
+ const allApps = await client.get("/api/assets", {
212
+ limit: 100,
213
+ offset: 0,
214
+ includeUnvalued: "true"
215
+ });
216
+ const match = (allApps.data ?? []).find(
217
+ (app2) => app2.bundleId === input.bundleId
218
+ );
157
219
  if (!match) {
158
220
  return {
159
221
  content: [
@@ -167,8 +229,13 @@ async function getAppDetails(input, client) {
167
229
  }
168
230
  assetId = match.id;
169
231
  }
170
- const response = await client.get(`/api/assets/${assetId}`);
171
- const app = response.data || response;
232
+ const response = await client.get(
233
+ `/api/assets/${assetId}`
234
+ );
235
+ const app = response.data;
236
+ if (!app) {
237
+ throw new Error("App details response did not include app data");
238
+ }
172
239
  return {
173
240
  content: [
174
241
  {
@@ -225,14 +292,27 @@ var sendReviewReplySchema = z2.object({
225
292
  });
226
293
  var translateReviewSchema = z2.object({
227
294
  reviewId: z2.string().describe("The review UUID to translate"),
228
- assetId: z2.string().uuid().describe("The app UUID the review belongs to")
295
+ assetId: z2.string().uuid().describe("The app UUID the review belongs to"),
296
+ // The translate endpoint receives the review content in the request body
297
+ // (it does not re-fetch it). Pass the fields from a get_reviews result.
298
+ rating: z2.number().int().min(1).max(5).describe("The review star rating, from get_reviews"),
299
+ title: z2.string().default("").describe("The review title, from get_reviews"),
300
+ body: z2.string().describe("The review text to translate, from get_reviews"),
301
+ storeFront: z2.string().default("").describe('The review storefront (e.g. "SE"), from get_reviews'),
302
+ appVersionString: z2.string().default("").describe("The app version the review was left on, from get_reviews")
229
303
  });
230
304
  async function getReviews(input, client) {
231
305
  try {
232
306
  let assetId = input.assetId;
233
307
  if (!assetId && input.bundleId) {
234
- const allApps = await client.get("/api/assets", { limit: 100, offset: 0 });
235
- const match = (allApps.data || []).find((a) => a.bundleId === input.bundleId);
308
+ const allApps = await client.get("/api/assets", {
309
+ limit: 100,
310
+ offset: 0,
311
+ includeUnvalued: "true"
312
+ });
313
+ const match = (allApps.data ?? []).find(
314
+ (app) => app.bundleId === input.bundleId
315
+ );
236
316
  if (!match) {
237
317
  return {
238
318
  content: [
@@ -246,12 +326,20 @@ async function getReviews(input, client) {
246
326
  }
247
327
  assetId = match.id;
248
328
  }
329
+ if (!assetId) {
330
+ return {
331
+ content: [
332
+ {
333
+ type: "text",
334
+ text: "Provide an assetId (or bundleId) to fetch reviews for. Use list_apps to find app IDs."
335
+ }
336
+ ],
337
+ isError: true
338
+ };
339
+ }
249
340
  const params = {
250
- limit: input.limit,
251
- sortBy: input.sortBy
341
+ limit: input.limit
252
342
  };
253
- if (assetId)
254
- params.assetId = assetId;
255
343
  if (input.platform)
256
344
  params.platform = input.platform;
257
345
  if (input.rating !== void 0)
@@ -262,13 +350,25 @@ async function getReviews(input, client) {
262
350
  params.startDate = input.startDate;
263
351
  if (input.endDate)
264
352
  params.endDate = input.endDate;
265
- const response = await client.get("/api/reviews", params);
266
- const reviews = response.data || response.reviews || [];
353
+ const response = await client.get(
354
+ `/api/assets/${assetId}/reviews`,
355
+ params
356
+ );
357
+ let reviews = response.data?.reviews ?? [];
358
+ if (input.sortBy === "rating") {
359
+ reviews = [...reviews].sort(
360
+ (a, b) => Number(b.rating ?? 0) - Number(a.rating ?? 0)
361
+ );
362
+ }
363
+ const hasReply = (review) => !!(review.developerResponse || review.replySource || review.hasReply);
267
364
  const summary = {
268
365
  totalReviews: reviews.length,
269
- averageRating: reviews.length > 0 ? (reviews.reduce((sum, r) => sum + (r.rating || 0), 0) / reviews.length).toFixed(2) : 0,
270
- repliedCount: reviews.filter((r) => r.developerResponse || r.hasReply).length,
271
- unrepliedCount: reviews.filter((r) => !r.developerResponse && !r.hasReply).length
366
+ averageRating: reviews.length > 0 ? (reviews.reduce((sum, review) => {
367
+ const rating = Number(review.rating ?? 0);
368
+ return sum + (Number.isFinite(rating) ? rating : 0);
369
+ }, 0) / reviews.length).toFixed(2) : 0,
370
+ repliedCount: reviews.filter((review) => hasReply(review)).length,
371
+ unrepliedCount: reviews.filter((review) => !hasReply(review)).length
272
372
  };
273
373
  return {
274
374
  content: [
@@ -276,19 +376,23 @@ async function getReviews(input, client) {
276
376
  type: "text",
277
377
  text: formatAsJson({
278
378
  summary,
279
- reviews: reviews.map((r) => ({
280
- id: r.id,
281
- appId: r.appId,
282
- platform: r.platform,
283
- rating: r.rating,
284
- title: r.title,
285
- body: r.body,
286
- author: r.nickname || r.author,
287
- date: r.lastModified || r.date,
288
- version: r.appVersionString || r.version,
289
- storeFront: r.storeFront,
290
- hasReply: !!(r.developerResponse || r.hasReply),
291
- reply: r.developerResponse ? typeof r.developerResponse === "object" ? r.developerResponse.response : r.developerResponse : r.reply || null
379
+ reviews: reviews.map((review) => ({
380
+ id: review.id,
381
+ appId: review.appId,
382
+ platform: review.platform,
383
+ rating: review.rating,
384
+ title: review.title,
385
+ // The handler exposes the review text as `review`.
386
+ body: review.review ?? review.body,
387
+ author: review.nickname || review.author,
388
+ // Epoch ms from the handler.
389
+ date: review.lastModified || review.date,
390
+ version: review.appVersionString || review.version,
391
+ storeFront: review.storeFront,
392
+ hasReply: hasReply(review),
393
+ replySource: review.replySource ?? null,
394
+ hasDraft: review.hasDraft ?? false,
395
+ reply: review.developerResponse ? typeof review.developerResponse === "object" ? review.developerResponse.response : review.developerResponse : review.reply || null
292
396
  }))
293
397
  })
294
398
  }
@@ -308,9 +412,10 @@ async function getReviews(input, client) {
308
412
  }
309
413
  async function generateReviewReply(input, client) {
310
414
  try {
311
- const response = await client.post(`/api/reviews/${input.reviewId}/generate-reply`, {
312
- assetId: input.assetId
313
- });
415
+ const response = await client.post(
416
+ `/api/assets/${input.assetId}/reviews/${input.reviewId}/generate-reply`,
417
+ void 0
418
+ );
314
419
  return {
315
420
  content: [
316
421
  {
@@ -333,10 +438,11 @@ async function generateReviewReply(input, client) {
333
438
  }
334
439
  async function sendReviewReply(input, client) {
335
440
  try {
336
- const response = await client.post(`/api/reviews/${input.reviewId}/respond`, {
337
- assetId: input.assetId,
338
- response: input.response
339
- });
441
+ const response = await client.post(
442
+ `/api/assets/${input.assetId}/reviews/${input.reviewId}/respond`,
443
+ // The API handler reads `responseText` from the body.
444
+ { responseText: input.response }
445
+ );
340
446
  return {
341
447
  content: [
342
448
  {
@@ -359,9 +465,18 @@ async function sendReviewReply(input, client) {
359
465
  }
360
466
  async function translateReview(input, client) {
361
467
  try {
362
- const response = await client.post(`/api/reviews/${input.reviewId}/translate`, {
363
- assetId: input.assetId
364
- });
468
+ const response = await client.post(
469
+ `/api/assets/${input.assetId}/reviews/${input.reviewId}/translate`,
470
+ // The translate endpoint expects the review content in the body.
471
+ {
472
+ rating: input.rating,
473
+ title: input.title,
474
+ review: input.body,
475
+ storeFront: input.storeFront,
476
+ appVersionString: input.appVersionString,
477
+ lastModified: Date.now()
478
+ }
479
+ );
365
480
  return {
366
481
  content: [
367
482
  {
@@ -441,9 +556,11 @@ var DIMENSION_INFO = {
441
556
  };
442
557
  async function discoverMetrics(input, client) {
443
558
  try {
444
- const response = await client.get(`/api/assets/${input.assetId}/metrics/availability`);
445
- const availableMetrics = response.data?.availableMetrics || [];
446
- const metricGroups = response.data?.metricGroups || {};
559
+ const response = await client.get(
560
+ `/api/assets/${input.assetId}/metrics/availability`
561
+ );
562
+ const availableMetrics = response.data?.availableMetrics ?? [];
563
+ const metricGroups = response.data?.metricGroups ?? {};
447
564
  const enriched = availableMetrics.map((metricName) => {
448
565
  const info = METRIC_INFO[metricName];
449
566
  return {
@@ -494,10 +611,13 @@ async function getMetrics(input, client) {
494
611
  if (input.dimensionFilter && input.dimension) {
495
612
  params[`filter.${input.dimension}`] = input.dimensionFilter;
496
613
  }
497
- const response = await client.get(`/api/assets/${input.assetId}/metrics/timeseries`, params);
498
- const data = response.data || {};
499
- const changes = response.changes || {};
500
- const meta = response.meta || {};
614
+ const response = await client.get(
615
+ `/api/assets/${input.assetId}/metrics/timeseries`,
616
+ params
617
+ );
618
+ const data = response.data ?? {};
619
+ const changes = response.changes ?? {};
620
+ const meta = response.meta ?? {};
501
621
  const summaries = {};
502
622
  for (const [metricName, timeseries] of Object.entries(data)) {
503
623
  const points = timeseries;
@@ -553,17 +673,20 @@ async function discoverDimensions(input, client) {
553
673
  assetId: input.assetId,
554
674
  dimension: input.dimension,
555
675
  displayName: DIMENSION_INFO[input.dimension] || input.dimension,
556
- values: response2.data || response2
676
+ values: response2.data ?? response2
557
677
  })
558
678
  }]
559
679
  };
560
680
  }
561
681
  const response = await client.get(`/api/assets/${input.assetId}/metrics/dimensions`);
562
- const dimensions = response.data || response || [];
563
- const enriched = (Array.isArray(dimensions) ? dimensions : Object.keys(dimensions)).map((dim) => ({
564
- name: dim,
565
- displayName: DIMENSION_INFO[dim] || dim
566
- }));
682
+ const dimensions = Array.isArray(response.data) ? response.data : [];
683
+ const enriched = dimensions.map((dim) => {
684
+ if (typeof dim === "string") {
685
+ return { name: dim, displayName: DIMENSION_INFO[dim] || dim };
686
+ }
687
+ const id = dim.id ?? "";
688
+ return { name: id, displayName: dim.label ?? DIMENSION_INFO[id] ?? id };
689
+ });
567
690
  return {
568
691
  content: [{
569
692
  type: "text",
@@ -581,6 +704,9 @@ async function discoverDimensions(input, client) {
581
704
 
582
705
  // src/tools/agents.ts
583
706
  import { z as z4 } from "zod";
707
+ function asRecord(value) {
708
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
709
+ }
584
710
  var AGENT_TYPES = [
585
711
  "review",
586
712
  "monitoring",
@@ -629,7 +755,7 @@ async function listAgents(_input, client) {
629
755
  let apiAgents = null;
630
756
  try {
631
757
  const response = await client.get("/api/agents");
632
- apiAgents = response.data || response.agents || response;
758
+ apiAgents = Array.isArray(response) ? response : response.data ?? response.agents ?? null;
633
759
  } catch {
634
760
  }
635
761
  if (apiAgents && Array.isArray(apiAgents)) {
@@ -679,21 +805,26 @@ async function getAgentDetails(input, client) {
679
805
  const params = {};
680
806
  if (input.assetId)
681
807
  params.assetId = input.assetId;
682
- const response2 = await client.get(`/api/agents/${input.agentType}`, params);
683
- return {
684
- content: [
685
- {
686
- type: "text",
687
- text: formatAsJson(response2.data || response2)
688
- }
689
- ]
690
- };
808
+ const response2 = await client.get(`/api/agents/by-type/${input.agentType}`, params);
809
+ if (response2.agent) {
810
+ return {
811
+ content: [
812
+ {
813
+ type: "text",
814
+ text: formatAsJson({
815
+ agent: response2.agent,
816
+ recentRuns: response2.recentRuns ?? []
817
+ })
818
+ }
819
+ ]
820
+ };
821
+ }
691
822
  } catch {
692
823
  }
693
824
  const response = {
694
825
  type: input.agentType,
695
826
  description: AGENT_DESCRIPTIONS[input.agentType],
696
- message: "Detailed agent configuration is available through the platform dashboard."
827
+ message: `No ${input.agentType} agent is provisioned for this organization yet. Activate it from the Agents page or with trigger_agent_run once created.`
697
828
  };
698
829
  return {
699
830
  content: [
@@ -717,33 +848,44 @@ async function getAgentDetails(input, client) {
717
848
  }
718
849
  async function getAgentRunHistory(input, client) {
719
850
  try {
851
+ let agentId;
720
852
  try {
721
- const params = {
722
- limit: input.limit
723
- };
853
+ const byTypeParams = {};
724
854
  if (input.assetId)
725
- params.assetId = input.assetId;
726
- const response = await client.get(`/api/agents/${input.agentType}/runs`, params);
855
+ byTypeParams.assetId = input.assetId;
856
+ const byType = await client.get(
857
+ `/api/agents/by-type/${input.agentType}`,
858
+ byTypeParams
859
+ );
860
+ agentId = byType.agent?.id;
861
+ } catch {
862
+ }
863
+ if (!agentId) {
727
864
  return {
728
865
  content: [
729
866
  {
730
867
  type: "text",
731
868
  text: formatAsJson({
732
869
  agentType: input.agentType,
733
- ...response.data || response
870
+ runs: [],
871
+ message: `No ${input.agentType} agent is provisioned for this organization yet, so there is no run history.`
734
872
  })
735
873
  }
736
874
  ]
737
875
  };
738
- } catch {
739
876
  }
877
+ const response = await client.get(
878
+ `/api/agents/${agentId}/runs`,
879
+ { limit: input.limit }
880
+ );
740
881
  return {
741
882
  content: [
742
883
  {
743
884
  type: "text",
744
885
  text: formatAsJson({
745
886
  agentType: input.agentType,
746
- 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.`
887
+ agentId,
888
+ ...asRecord(response.data ?? response)
747
889
  })
748
890
  }
749
891
  ]
@@ -762,15 +904,16 @@ async function getAgentRunHistory(input, client) {
762
904
  }
763
905
  async function triggerAgentRun(input, client) {
764
906
  try {
765
- const body = {};
766
- if (input.assetId)
767
- body.assetId = input.assetId;
768
- const response = await client.post(`/api/agents/${input.agentId}/run`, body);
907
+ const body = input.assetId ? { triggerContext: { assetId: input.assetId } } : {};
908
+ const response = await client.post(
909
+ `/api/agents/${input.agentId}/run`,
910
+ body
911
+ );
769
912
  return {
770
913
  content: [
771
914
  {
772
915
  type: "text",
773
- text: formatAsJson(response.data || response)
916
+ text: formatAsJson(response.data ?? response)
774
917
  }
775
918
  ]
776
919
  };
@@ -788,12 +931,14 @@ async function triggerAgentRun(input, client) {
788
931
  }
789
932
  async function pauseAgent(input, client) {
790
933
  try {
791
- const response = await client.post(`/api/agents/${input.agentId}/pause`);
934
+ const response = await client.post(
935
+ `/api/agents/${input.agentId}/pause`
936
+ );
792
937
  return {
793
938
  content: [
794
939
  {
795
940
  type: "text",
796
- text: formatAsJson(response.data || response)
941
+ text: formatAsJson(response.data ?? response)
797
942
  }
798
943
  ]
799
944
  };
@@ -811,12 +956,14 @@ async function pauseAgent(input, client) {
811
956
  }
812
957
  async function resumeAgent(input, client) {
813
958
  try {
814
- const response = await client.post(`/api/agents/${input.agentId}/resume`);
959
+ const response = await client.post(
960
+ `/api/agents/${input.agentId}/resume`
961
+ );
815
962
  return {
816
963
  content: [
817
964
  {
818
965
  type: "text",
819
- text: formatAsJson(response.data || response)
966
+ text: formatAsJson(response.data ?? response)
820
967
  }
821
968
  ]
822
969
  };
@@ -834,12 +981,14 @@ async function resumeAgent(input, client) {
834
981
  }
835
982
  async function getAgentActivity(input, client) {
836
983
  try {
837
- const response = await client.get(`/api/agents/${input.agentId}/activity`);
984
+ const response = await client.get(
985
+ `/api/agents/${input.agentId}/activity`
986
+ );
838
987
  return {
839
988
  content: [
840
989
  {
841
990
  type: "text",
842
- text: formatAsJson(response.data || response)
991
+ text: formatAsJson(response.data ?? response)
843
992
  }
844
993
  ]
845
994
  };
@@ -897,34 +1046,40 @@ async function getAnomalies(input, client) {
897
1046
  params.fromDate = input.fromDate;
898
1047
  if (input.toDate)
899
1048
  params.toDate = input.toDate;
900
- if (input.excludeDismissed)
901
- params.excludeDismissed = input.excludeDismissed;
902
- const response = await client.get("/api/anomalies", params);
903
- const anomalies = response.data || response.anomalies || [];
1049
+ const response = await client.get(
1050
+ "/api/anomalies",
1051
+ params
1052
+ );
1053
+ let anomalies = response.data ?? response.anomalies ?? [];
1054
+ if (input.excludeDismissed && !input.status) {
1055
+ anomalies = anomalies.filter((a) => a.status !== "dismissed");
1056
+ }
904
1057
  return {
905
1058
  content: [
906
1059
  {
907
1060
  type: "text",
908
1061
  text: formatAsJson({
909
1062
  total: anomalies.length,
910
- anomalies: anomalies.map((a) => ({
911
- id: a.id,
912
- assetId: a.assetId,
913
- assetName: a.assetName,
914
- assetIcon: a.assetIcon,
915
- anomalyDate: a.anomalyDate,
916
- metricName: a.metricName,
917
- sourceType: a.sourceType,
918
- type: a.type,
919
- severity: a.severity,
920
- actualValue: parseFloat(a.actualValue || "0"),
921
- expectedValue: parseFloat(a.expectedValue || "0"),
922
- deviationPercent: parseFloat(a.deviationPercent || "0"),
923
- confidence: parseFloat(a.confidence || "0"),
924
- explanation: a.explanation,
925
- suggestedActions: a.suggestedActions,
926
- status: a.status,
927
- detectedAt: a.detectedAt
1063
+ anomalies: anomalies.map((anomaly) => ({
1064
+ id: anomaly.id,
1065
+ assetId: anomaly.assetId,
1066
+ assetName: anomaly.assetName,
1067
+ assetIcon: anomaly.assetIcon,
1068
+ anomalyDate: anomaly.anomalyDate,
1069
+ metricName: anomaly.metricName,
1070
+ sourceType: anomaly.sourceType,
1071
+ type: anomaly.type,
1072
+ severity: anomaly.severity,
1073
+ actualValue: parseFloat(String(anomaly.actualValue || "0")),
1074
+ expectedValue: parseFloat(String(anomaly.expectedValue || "0")),
1075
+ deviationPercent: parseFloat(
1076
+ String(anomaly.deviationPercent || "0")
1077
+ ),
1078
+ confidence: parseFloat(String(anomaly.confidence || "0")),
1079
+ explanation: anomaly.explanation,
1080
+ suggestedActions: anomaly.suggestedActions,
1081
+ status: anomaly.status,
1082
+ detectedAt: anomaly.detectedAt
928
1083
  }))
929
1084
  })
930
1085
  }
@@ -1023,37 +1178,46 @@ var getAdsPerformanceSchema = z6.object({
1023
1178
  });
1024
1179
  async function getAdsPerformance(input, client) {
1025
1180
  try {
1026
- const params = {
1027
- limit: input.limit
1028
- };
1181
+ const params = {};
1029
1182
  if (input.assetId)
1030
1183
  params.assetId = input.assetId;
1031
- if (input.platform)
1032
- params.platform = input.platform;
1033
1184
  if (input.fromDate)
1034
- params.fromDate = input.fromDate;
1185
+ params.from = input.fromDate;
1035
1186
  if (input.toDate)
1036
- params.toDate = input.toDate;
1037
- const response = await client.get("/api/ads/campaigns", params);
1038
- const campaigns = response.data || response.campaigns || [];
1187
+ params.to = input.toDate;
1188
+ const response = await client.get(
1189
+ "/api/ads-agent/campaigns",
1190
+ params
1191
+ );
1192
+ let campaigns = response.data ?? response.campaigns ?? [];
1193
+ if (input.platform) {
1194
+ campaigns = campaigns.filter((c) => c.platform === input.platform);
1195
+ }
1196
+ campaigns = campaigns.slice(0, input.limit);
1039
1197
  return {
1040
1198
  content: [
1041
1199
  {
1042
1200
  type: "text",
1043
1201
  text: formatAsJson({
1044
1202
  total: campaigns.length,
1045
- campaigns: campaigns.map((c) => ({
1046
- id: c.id,
1047
- assetId: c.assetId,
1048
- assetName: c.assetName,
1049
- platformCampaignId: c.platformCampaignId,
1050
- name: c.name,
1051
- status: c.status,
1052
- objective: c.objective,
1053
- platform: c.platform,
1054
- linkSource: c.linkSource,
1055
- linkedAt: c.linkedAt,
1056
- recentPerformance: c.recentPerformance || null
1203
+ campaigns: campaigns.map((campaign) => ({
1204
+ id: campaign.id,
1205
+ assetId: campaign.assetId,
1206
+ assetName: campaign.assetName,
1207
+ platformCampaignId: campaign.platformCampaignId,
1208
+ name: campaign.name,
1209
+ status: campaign.status,
1210
+ objective: campaign.objective,
1211
+ platform: campaign.platform,
1212
+ linkSource: campaign.linkSource,
1213
+ linkedAt: campaign.linkedAt,
1214
+ totalSpend: campaign.totalSpend ?? null,
1215
+ totalImpressions: campaign.totalImpressions ?? null,
1216
+ totalTaps: campaign.totalTaps ?? null,
1217
+ totalInstalls: campaign.totalInstalls ?? null,
1218
+ avgCPI: campaign.avgCPI ?? null,
1219
+ avgTTR: campaign.avgTTR ?? null,
1220
+ avgCVR: campaign.avgCVR ?? null
1057
1221
  }))
1058
1222
  })
1059
1223
  }
@@ -1131,13 +1295,17 @@ async function getGrowthScore(input, client) {
1131
1295
  import { z as z8 } from "zod";
1132
1296
  var getForecastsSchema = z8.object({
1133
1297
  assetId: z8.string().uuid().describe("App UUID to get forecasts for"),
1134
- dataPoints: z8.number().int().min(4).max(52).default(12).describe("Number of historical data points to include")
1298
+ metric: z8.enum(["proceeds", "total_revenue", "total_downloads", "active_subs", "new_trials"]).describe("The metric to forecast"),
1299
+ forecastWeeks: z8.number().int().min(4).max(156).optional().describe("Weeks to forecast forward (default 104)"),
1300
+ historicalWeeks: z8.number().int().min(12).max(156).optional().describe("Weeks of history to base the forecast on (default 104)")
1135
1301
  });
1136
1302
  async function getForecasts(input, client) {
1137
1303
  try {
1138
1304
  const response = await client.get("/api/forecasting/forecast", {
1139
1305
  assetId: input.assetId,
1140
- dataPoints: input.dataPoints
1306
+ metricName: input.metric,
1307
+ forecastWeeks: input.forecastWeeks,
1308
+ historicalWeeks: input.historicalWeeks
1141
1309
  });
1142
1310
  return {
1143
1311
  content: [
@@ -1162,18 +1330,34 @@ async function getForecasts(input, client) {
1162
1330
 
1163
1331
  // src/tools/dashboard.ts
1164
1332
  import { z as z9 } from "zod";
1333
+ function parseDashboardNumber(value) {
1334
+ if (value === null || value === void 0 || value === "")
1335
+ return null;
1336
+ const parsed = typeof value === "number" ? value : Number.parseFloat(value);
1337
+ return Number.isFinite(parsed) ? parsed : null;
1338
+ }
1339
+ function getSidebarAssets(response) {
1340
+ if (!response)
1341
+ return [];
1342
+ if (Array.isArray(response))
1343
+ return response;
1344
+ return response.data ?? [];
1345
+ }
1165
1346
  var getDashboardOverviewSchema = z9.object({});
1166
1347
  async function getDashboardOverview(_input, client) {
1167
1348
  try {
1349
+ const overviewPromise = client.get("/api/dashboard/overview-metrics").catch(() => null);
1350
+ const sidebarPromise = client.get(
1351
+ "/api/dashboard/sidebar-assets"
1352
+ ).catch(() => null);
1168
1353
  const [overviewResponse, sidebarResponse] = await Promise.all([
1169
- client.get("/api/dashboard/overview-metrics").catch(() => null),
1170
- client.get("/api/dashboard/sidebar-assets").catch(() => null)
1354
+ overviewPromise,
1355
+ sidebarPromise
1171
1356
  ]);
1172
- const overview = overviewResponse?.data || overviewResponse || {};
1173
- const sidebarAssets = sidebarResponse?.data || sidebarResponse || [];
1174
- const apps = Array.isArray(sidebarAssets) ? sidebarAssets : [];
1175
- const totalValuation = apps.reduce((sum, a) => {
1176
- return sum + parseFloat(a.currentValuation || "0");
1357
+ const overview = overviewResponse?.data ?? overviewResponse ?? {};
1358
+ const apps = getSidebarAssets(sidebarResponse);
1359
+ const totalValuation = apps.reduce((sum, app) => {
1360
+ return sum + (parseDashboardNumber(app.currentValuation) ?? 0);
1177
1361
  }, 0);
1178
1362
  return {
1179
1363
  content: [
@@ -1183,16 +1367,16 @@ async function getDashboardOverview(_input, client) {
1183
1367
  portfolio: {
1184
1368
  totalApps: apps.length,
1185
1369
  totalValuation: totalValuation > 0 ? totalValuation : null,
1186
- apps: apps.map((a) => ({
1187
- id: a.id,
1188
- name: a.name,
1189
- bundleId: a.bundleId,
1190
- platform: a.appleAppId ? "ios" : a.googleAppId ? "android" : "unknown",
1191
- currentValuation: a.currentValuation ? parseFloat(a.currentValuation) : null,
1192
- rating: a.rating ? parseFloat(a.rating) : null,
1193
- ratingCount: a.ratingCount || null,
1194
- category: a.category || null,
1195
- iconUrl: a.iconUrl || null
1370
+ apps: apps.map((app) => ({
1371
+ id: app.id,
1372
+ name: app.name,
1373
+ bundleId: app.bundleId,
1374
+ platform: app.appleAppId ? "ios" : app.googleAppId ? "android" : "unknown",
1375
+ currentValuation: parseDashboardNumber(app.currentValuation),
1376
+ rating: parseDashboardNumber(app.rating),
1377
+ ratingCount: app.ratingCount ?? null,
1378
+ category: app.category ?? null,
1379
+ iconUrl: app.iconUrl ?? null
1196
1380
  }))
1197
1381
  },
1198
1382
  overview
@@ -1215,13 +1399,15 @@ async function getDashboardOverview(_input, client) {
1215
1399
 
1216
1400
  // src/tools/actions.ts
1217
1401
  import { z as z10 } from "zod";
1402
+ function asRecord2(value) {
1403
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1404
+ }
1218
1405
  var listPendingActionsSchema = z10.object({
1219
1406
  assetId: z10.string().uuid().optional().describe("Filter by app UUID"),
1220
1407
  limit: z10.number().int().min(1).max(100).default(50).describe("Maximum number of actions to return")
1221
1408
  });
1222
1409
  var approveActionSchema = z10.object({
1223
- actionId: z10.string().describe("The draft reply ID to approve"),
1224
- editedReply: z10.string().optional().describe("Optionally modify the reply text before approving")
1410
+ actionId: z10.string().describe("The pending action ID to approve")
1225
1411
  });
1226
1412
  var rejectActionSchema = z10.object({
1227
1413
  actionId: z10.string().describe("The draft reply ID to reject/delete")
@@ -1231,10 +1417,16 @@ async function listPendingActions(input, client) {
1231
1417
  const params = {
1232
1418
  limit: input.limit
1233
1419
  };
1234
- if (input.assetId)
1235
- params.assetId = input.assetId;
1236
- const response = await client.get("/api/pending-actions", params);
1237
- const actions = response.data || response.actions || [];
1420
+ const response = await client.get(
1421
+ "/api/pending-actions",
1422
+ params
1423
+ );
1424
+ let actions = response.data ?? response.actions ?? [];
1425
+ if (input.assetId) {
1426
+ actions = actions.filter(
1427
+ (action) => action.assetId === input.assetId
1428
+ );
1429
+ }
1238
1430
  return {
1239
1431
  content: [
1240
1432
  {
@@ -1260,10 +1452,10 @@ async function listPendingActions(input, client) {
1260
1452
  }
1261
1453
  async function approveAction(input, client) {
1262
1454
  try {
1263
- const body = {};
1264
- if (input.editedReply)
1265
- body.editedReply = input.editedReply;
1266
- const response = await client.post(`/api/pending-actions/${input.actionId}/approve`, body);
1455
+ const response = await client.post(
1456
+ `/api/pending-actions/${input.actionId}/approve`,
1457
+ {}
1458
+ );
1267
1459
  return {
1268
1460
  content: [
1269
1461
  {
@@ -1273,7 +1465,7 @@ async function approveAction(input, client) {
1273
1465
  actionId: input.actionId,
1274
1466
  status: "approved",
1275
1467
  message: "Reply has been approved and queued for sending",
1276
- ...response.data || {}
1468
+ ...asRecord2(response.data)
1277
1469
  })
1278
1470
  }
1279
1471
  ]
@@ -1292,7 +1484,9 @@ async function approveAction(input, client) {
1292
1484
  }
1293
1485
  async function rejectAction(input, client) {
1294
1486
  try {
1295
- const response = await client.post(`/api/pending-actions/${input.actionId}/reject`);
1487
+ const response = await client.post(
1488
+ `/api/pending-actions/${input.actionId}/reject`
1489
+ );
1296
1490
  return {
1297
1491
  content: [
1298
1492
  {
@@ -1302,7 +1496,7 @@ async function rejectAction(input, client) {
1302
1496
  actionId: input.actionId,
1303
1497
  status: "rejected",
1304
1498
  message: "Draft reply has been rejected and removed",
1305
- ...response.data || {}
1499
+ ...asRecord2(response.data)
1306
1500
  })
1307
1501
  }
1308
1502
  ]
@@ -1336,10 +1530,13 @@ var getAsoExperimentsSchema = z11.object({
1336
1530
  assetId: z11.string().uuid().describe("App UUID to list ASO experiments for"),
1337
1531
  status: z11.enum(["proposed", "approved", "applied", "measuring", "completed", "reverted"]).optional().describe("Filter experiments by status"),
1338
1532
  limit: z11.number().int().min(1).max(100).default(20).describe("Maximum number of experiments to return"),
1339
- offset: z11.number().int().min(0).default(0).describe("Number of experiments to skip for pagination")
1533
+ cursor: z11.string().optional().describe("Pagination cursor from a previous response (meta.nextCursor)")
1340
1534
  });
1341
1535
  var getAsoLocaleSnapshotsSchema = z11.object({
1342
- assetId: z11.string().uuid().describe("App UUID to get locale snapshots for")
1536
+ assetId: z11.string().uuid().describe("App UUID to get locale snapshots for"),
1537
+ store: z11.enum(["ios", "android"]).default("ios").describe("Which store listing to read"),
1538
+ locale: z11.string().optional().describe('Filter to a single locale (e.g. "en-US"). Returns all locales if omitted.'),
1539
+ limit: z11.number().int().min(1).max(500).optional().describe("Maximum snapshot rows to return (newest first)")
1343
1540
  });
1344
1541
  var triggerAsoAnalysisSchema = z11.object({
1345
1542
  assetId: z11.string().uuid().describe("App UUID to trigger ASO analysis for")
@@ -1420,7 +1617,7 @@ async function getAsoExperiments(input, client) {
1420
1617
  try {
1421
1618
  const params = {
1422
1619
  limit: input.limit,
1423
- offset: input.offset
1620
+ cursor: input.cursor
1424
1621
  };
1425
1622
  if (input.status)
1426
1623
  params.status = input.status;
@@ -1447,7 +1644,14 @@ async function getAsoExperiments(input, client) {
1447
1644
  }
1448
1645
  async function getAsoLocaleSnapshots(input, client) {
1449
1646
  try {
1450
- const response = await client.get(`/api/assets/${input.assetId}/aso/locale-snapshots`);
1647
+ const response = await client.get(
1648
+ `/api/assets/${input.assetId}/aso/listing-snapshots`,
1649
+ {
1650
+ store: input.store,
1651
+ locale: input.locale,
1652
+ limit: input.limit
1653
+ }
1654
+ );
1451
1655
  return {
1452
1656
  content: [
1453
1657
  {
@@ -1470,7 +1674,10 @@ async function getAsoLocaleSnapshots(input, client) {
1470
1674
  }
1471
1675
  async function triggerAsoAnalysis(input, client) {
1472
1676
  try {
1473
- const response = await client.post(`/api/assets/${input.assetId}/aso/analyze`);
1677
+ const response = await client.post(
1678
+ `/api/assets/${input.assetId}/aso/refetch`,
1679
+ { force: true }
1680
+ );
1474
1681
  return {
1475
1682
  content: [
1476
1683
  {
@@ -1493,6 +1700,7 @@ async function triggerAsoAnalysis(input, client) {
1493
1700
  }
1494
1701
 
1495
1702
  // src/tools/chat.ts
1703
+ import { randomUUID } from "node:crypto";
1496
1704
  import { z as z12 } from "zod";
1497
1705
  var listConversationsSchema = z12.object({
1498
1706
  limit: z12.number().int().min(1).max(100).default(20).describe("Maximum number of conversations to return")
@@ -1562,23 +1770,81 @@ async function getConversationMessages(input, client) {
1562
1770
  };
1563
1771
  }
1564
1772
  }
1773
+ function parseUiMessageStream(raw) {
1774
+ let reply = "";
1775
+ const toolsUsed = [];
1776
+ for (const line of raw.split("\n")) {
1777
+ if (!line.startsWith("data:"))
1778
+ continue;
1779
+ const payload = line.slice(5).trim();
1780
+ if (!payload || payload === "[DONE]")
1781
+ continue;
1782
+ try {
1783
+ const event = JSON.parse(payload);
1784
+ if (event.type === "text-delta") {
1785
+ reply += String(event.delta ?? event.textDelta ?? "");
1786
+ } else if (typeof event.type === "string" && event.type === "tool-input-start" && typeof event.toolName === "string") {
1787
+ toolsUsed.push(event.toolName);
1788
+ }
1789
+ } catch {
1790
+ }
1791
+ }
1792
+ return { reply, toolsUsed };
1793
+ }
1565
1794
  async function sendChatMessage(input, client) {
1566
1795
  try {
1796
+ const conversationId = input.conversationId ?? randomUUID();
1797
+ let priorMessages = [];
1798
+ if (input.conversationId) {
1799
+ const history = await client.get(`/api/chat/messages/${input.conversationId}`, { limit: 1e3 });
1800
+ for (const message of history.data ?? []) {
1801
+ const textParts = (message.parts ?? []).filter(
1802
+ (part) => part.type === "text" && typeof part.text === "string"
1803
+ );
1804
+ if (textParts.length === 0)
1805
+ continue;
1806
+ priorMessages.push({
1807
+ id: message.id,
1808
+ role: message.role,
1809
+ parts: textParts
1810
+ });
1811
+ }
1812
+ priorMessages = priorMessages.slice(-100);
1813
+ }
1567
1814
  const body = {
1568
- message: input.message
1815
+ id: conversationId,
1816
+ messages: [
1817
+ ...priorMessages,
1818
+ {
1819
+ id: randomUUID(),
1820
+ role: "user",
1821
+ parts: [{ type: "text", text: input.message }]
1822
+ }
1823
+ ]
1569
1824
  };
1570
- if (input.conversationId)
1571
- body.conversationId = input.conversationId;
1572
- if (input.agentType)
1573
- body.agentType = input.agentType;
1574
- const response = await client.post("/api/chat", body);
1825
+ if (input.agentType) {
1826
+ body.agentContext = {
1827
+ agentId: null,
1828
+ agentType: input.agentType,
1829
+ agentName: input.agentType,
1830
+ surface: "mcp"
1831
+ };
1832
+ }
1833
+ const raw = await client.postStream("/api/chat", body);
1834
+ const { reply, toolsUsed } = parseUiMessageStream(raw);
1575
1835
  return {
1576
1836
  content: [
1577
1837
  {
1578
1838
  type: "text",
1579
- text: formatAsJson(response.data || response)
1839
+ text: formatAsJson({
1840
+ conversationId,
1841
+ reply: reply || "The assistant returned no text response.",
1842
+ toolsUsed,
1843
+ tip: "Pass this conversationId to continue the conversation or to get_conversation_messages for the full transcript."
1844
+ })
1580
1845
  }
1581
- ]
1846
+ ],
1847
+ ...reply ? {} : { isError: true }
1582
1848
  };
1583
1849
  } catch (error) {
1584
1850
  return {
@@ -1596,8 +1862,8 @@ async function sendChatMessage(input, client) {
1596
1862
  // src/tools/index.ts
1597
1863
  function registerTools(server, client, options = {}) {
1598
1864
  const rateLimiter = options.rateLimiter ?? new RateLimiter(100, 6e4);
1599
- function wrapTool(_toolName, handler) {
1600
- return async (input) => {
1865
+ function wrapTool(handler) {
1866
+ const callback = async (input) => {
1601
1867
  const rateCheck = rateLimiter.check("default");
1602
1868
  if (!rateCheck.allowed) {
1603
1869
  return {
@@ -1614,6 +1880,7 @@ function registerTools(server, client, options = {}) {
1614
1880
  }
1615
1881
  return handler(input, client);
1616
1882
  };
1883
+ return callback;
1617
1884
  }
1618
1885
  function tool(name, title, description, schema, handler, annotations) {
1619
1886
  server.registerTool(
@@ -1624,8 +1891,7 @@ function registerTools(server, client, options = {}) {
1624
1891
  inputSchema: schema.shape,
1625
1892
  annotations: { title, ...annotations }
1626
1893
  },
1627
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1628
- wrapTool(name, handler)
1894
+ wrapTool(handler)
1629
1895
  );
1630
1896
  }
1631
1897
  tool(
@@ -1671,7 +1937,7 @@ function registerTools(server, client, options = {}) {
1671
1937
  tool(
1672
1938
  "translate_review",
1673
1939
  "Translate review",
1674
- "Translate a review to English. Useful for reviews written in other languages. Returns the translated text. Does not modify the review in Fload.",
1940
+ "Translate a review to English. Useful for reviews written in other languages. Pass the review content fields (rating, title, body, storeFront, appVersionString) from a get_reviews result. Returns the translated text. Does not modify the review in Fload.",
1675
1941
  translateReviewSchema,
1676
1942
  translateReview,
1677
1943
  { readOnlyHint: true, openWorldHint: true }
@@ -1815,7 +2081,7 @@ function registerTools(server, client, options = {}) {
1815
2081
  tool(
1816
2082
  "get_forecasts",
1817
2083
  "Get forecasts",
1818
- "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.",
2084
+ "Get a statistical forecast for an app metric (proceeds, total_revenue, total_downloads, active_subs, or new_trials). Returns weekly historical values plus a forward projection with confidence bounds.",
1819
2085
  getForecastsSchema,
1820
2086
  getForecasts,
1821
2087
  { readOnlyHint: true, openWorldHint: false }