@fload-ai/mcp 0.1.1 → 0.2.2

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