apphud-mcp 0.2.2 → 0.2.5

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.
@@ -0,0 +1,398 @@
1
+ import { z } from "zod";
2
+ import { ApphudMcpError } from "../errors/toolError.js";
3
+ import { resolveAuth } from "../security/authResolver.js";
4
+ const authSchema = z
5
+ .object({
6
+ tenant_id: z.string().min(1),
7
+ role: z.enum(["analyst", "support", "admin"]),
8
+ actor_id: z.string().min(1).optional(),
9
+ })
10
+ .optional();
11
+ const analyticsPlatformSchema = z.enum(["ios", "android", "web"]);
12
+ const analyticsFiltersSchema = z
13
+ .record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
14
+ .optional();
15
+ const analyticsEventsFiltersSchema = z
16
+ .object({
17
+ user_id: z.string().min(1).optional(),
18
+ product_id: z.string().min(1).optional(),
19
+ event_type: z.string().min(1).optional(),
20
+ country: z.string().min(1).optional(),
21
+ platform: analyticsPlatformSchema.optional(),
22
+ })
23
+ .optional();
24
+ const baseInputShape = {
25
+ tenant_id: z.string().optional(),
26
+ auth: authSchema,
27
+ };
28
+ export const REMOTE_TOOL_NAMES = [
29
+ "apphud_apps_list",
30
+ "apphud_analytics_events_list",
31
+ "apphud_analytics_active_subscriptions",
32
+ "apphud_analytics_capabilities_get",
33
+ "apphud_analytics_metrics_list",
34
+ "apphud_analytics_metric_value",
35
+ "apphud_analytics_metric_timeseries",
36
+ "apphud_analytics_metric_breakdown",
37
+ "apphud_analytics_revenue_summary",
38
+ "apphud_analytics_subscriptions_summary",
39
+ "apphud_analytics_conversion_trial_to_paid",
40
+ "apphud_analytics_cohorts_retention",
41
+ "apphud_analytics_cohorts_ltv",
42
+ "apphud_analytics_query_raw",
43
+ ];
44
+ const REMOTE_TOOL_NAME_SET = new Set(REMOTE_TOOL_NAMES);
45
+ function appIdFromInput(input) {
46
+ const appId = typeof input.app_id === "string" ? input.app_id.trim() : "";
47
+ const apphudAppId = typeof input.apphud_app_id === "string" ? input.apphud_app_id.trim() : "";
48
+ return appId || apphudAppId || undefined;
49
+ }
50
+ const REMOTE_TOOL_DEFINITIONS = {
51
+ apphud_apps_list: {
52
+ description: "List apps available in authenticated Apphud dashboard account",
53
+ inputShape: {
54
+ ...baseInputShape,
55
+ include_raw: z.boolean().optional(),
56
+ },
57
+ execute: async (container, auth, input) => container.analyticsService.appsList(auth, {
58
+ include_raw: input.include_raw,
59
+ }),
60
+ },
61
+ apphud_analytics_events_list: {
62
+ description: "List raw subscription events from Apphud dashboard event feed",
63
+ inputShape: {
64
+ ...baseInputShape,
65
+ app_id: z.string().min(1).optional(),
66
+ apphud_app_id: z.string().min(1).optional(),
67
+ from: z.string().datetime(),
68
+ to: z.string().datetime(),
69
+ limit: z.number().int().min(1).max(500).optional(),
70
+ cursor: z.string().min(1).optional(),
71
+ filters: analyticsEventsFiltersSchema,
72
+ include_raw: z.boolean().optional(),
73
+ },
74
+ appIdSelector: appIdFromInput,
75
+ execute: async (container, auth, input) => container.analyticsService.eventsList(auth, {
76
+ app_id: input.app_id,
77
+ apphud_app_id: input.apphud_app_id,
78
+ from: input.from,
79
+ to: input.to,
80
+ limit: input.limit,
81
+ cursor: input.cursor,
82
+ filters: input.filters,
83
+ include_raw: input.include_raw,
84
+ }),
85
+ },
86
+ apphud_analytics_active_subscriptions: {
87
+ description: "Fetch active paid subscriptions directly from Apphud analytics endpoint",
88
+ inputShape: {
89
+ ...baseInputShape,
90
+ app_id: z.string().min(1).optional(),
91
+ apphud_app_id: z.string().min(1).optional(),
92
+ platform: analyticsPlatformSchema.optional(),
93
+ include_raw: z.boolean().optional(),
94
+ },
95
+ appIdSelector: appIdFromInput,
96
+ execute: async (container, auth, input) => container.analyticsService.activeSubscriptions(auth, {
97
+ app_id: input.app_id,
98
+ apphud_app_id: input.apphud_app_id,
99
+ platform: input.platform,
100
+ include_raw: input.include_raw,
101
+ }),
102
+ },
103
+ apphud_analytics_capabilities_get: {
104
+ description: "Get analytics capabilities and available metrics",
105
+ inputShape: {
106
+ ...baseInputShape,
107
+ app_id: z.string().min(1).optional(),
108
+ apphud_app_id: z.string().min(1).optional(),
109
+ include_raw: z.boolean().optional(),
110
+ },
111
+ appIdSelector: appIdFromInput,
112
+ execute: async (container, auth, input) => container.analyticsService.capabilities(auth, {
113
+ app_id: input.app_id,
114
+ apphud_app_id: input.apphud_app_id,
115
+ include_raw: input.include_raw,
116
+ }),
117
+ },
118
+ apphud_analytics_metrics_list: {
119
+ description: "List analytics metric keys",
120
+ inputShape: {
121
+ ...baseInputShape,
122
+ app_id: z.string().min(1).optional(),
123
+ apphud_app_id: z.string().min(1).optional(),
124
+ include_raw: z.boolean().optional(),
125
+ },
126
+ appIdSelector: appIdFromInput,
127
+ execute: async (container, auth, input) => container.analyticsService.metricsList(auth, {
128
+ app_id: input.app_id,
129
+ apphud_app_id: input.apphud_app_id,
130
+ include_raw: input.include_raw,
131
+ }),
132
+ },
133
+ apphud_analytics_metric_value: {
134
+ description: "Get single metric value for period/snapshot",
135
+ inputShape: {
136
+ ...baseInputShape,
137
+ app_id: z.string().min(1).optional(),
138
+ apphud_app_id: z.string().min(1).optional(),
139
+ metric_key: z.string().min(1),
140
+ from: z.string().datetime().optional(),
141
+ to: z.string().datetime().optional(),
142
+ platform: analyticsPlatformSchema.optional(),
143
+ filters: analyticsFiltersSchema,
144
+ include_raw: z.boolean().optional(),
145
+ },
146
+ appIdSelector: appIdFromInput,
147
+ execute: async (container, auth, input) => container.analyticsService.metricValue(auth, {
148
+ app_id: input.app_id,
149
+ apphud_app_id: input.apphud_app_id,
150
+ metric_key: input.metric_key,
151
+ from: input.from,
152
+ to: input.to,
153
+ platform: input.platform,
154
+ filters: input.filters,
155
+ include_raw: input.include_raw,
156
+ }),
157
+ },
158
+ apphud_analytics_metric_timeseries: {
159
+ description: "Get metric timeseries by date range",
160
+ inputShape: {
161
+ ...baseInputShape,
162
+ app_id: z.string().min(1).optional(),
163
+ apphud_app_id: z.string().min(1).optional(),
164
+ metric_key: z.string().min(1),
165
+ from: z.string().datetime(),
166
+ to: z.string().datetime(),
167
+ granularity: z.enum(["day", "week"]).optional(),
168
+ platform: analyticsPlatformSchema.optional(),
169
+ filters: analyticsFiltersSchema,
170
+ include_raw: z.boolean().optional(),
171
+ },
172
+ appIdSelector: appIdFromInput,
173
+ execute: async (container, auth, input) => container.analyticsService.metricTimeseries(auth, {
174
+ app_id: input.app_id,
175
+ apphud_app_id: input.apphud_app_id,
176
+ metric_key: input.metric_key,
177
+ from: input.from,
178
+ to: input.to,
179
+ granularity: input.granularity,
180
+ platform: input.platform,
181
+ filters: input.filters,
182
+ include_raw: input.include_raw,
183
+ }),
184
+ },
185
+ apphud_analytics_metric_breakdown: {
186
+ description: "Get metric breakdown by dimension",
187
+ inputShape: {
188
+ ...baseInputShape,
189
+ app_id: z.string().min(1).optional(),
190
+ apphud_app_id: z.string().min(1).optional(),
191
+ metric_key: z.string().min(1),
192
+ from: z.string().datetime(),
193
+ to: z.string().datetime(),
194
+ dimension: z.string().min(1),
195
+ granularity: z.enum(["day", "week"]).optional(),
196
+ platform: analyticsPlatformSchema.optional(),
197
+ filters: analyticsFiltersSchema,
198
+ limit: z.number().int().min(1).max(200).optional(),
199
+ include_raw: z.boolean().optional(),
200
+ },
201
+ appIdSelector: appIdFromInput,
202
+ execute: async (container, auth, input) => container.analyticsService.metricBreakdown(auth, {
203
+ app_id: input.app_id,
204
+ apphud_app_id: input.apphud_app_id,
205
+ metric_key: input.metric_key,
206
+ from: input.from,
207
+ to: input.to,
208
+ dimension: input.dimension,
209
+ granularity: input.granularity,
210
+ platform: input.platform,
211
+ filters: input.filters,
212
+ limit: input.limit,
213
+ include_raw: input.include_raw,
214
+ }),
215
+ },
216
+ apphud_analytics_revenue_summary: {
217
+ description: "Get revenue summary for date range",
218
+ inputShape: {
219
+ ...baseInputShape,
220
+ app_id: z.string().min(1).optional(),
221
+ apphud_app_id: z.string().min(1).optional(),
222
+ from: z.string().datetime(),
223
+ to: z.string().datetime(),
224
+ platform: analyticsPlatformSchema.optional(),
225
+ filters: analyticsFiltersSchema,
226
+ compare_prev_period: z.boolean().optional(),
227
+ include_raw: z.boolean().optional(),
228
+ },
229
+ appIdSelector: appIdFromInput,
230
+ execute: async (container, auth, input) => container.analyticsService.revenueSummary(auth, {
231
+ app_id: input.app_id,
232
+ apphud_app_id: input.apphud_app_id,
233
+ from: input.from,
234
+ to: input.to,
235
+ platform: input.platform,
236
+ filters: input.filters,
237
+ compare_prev_period: input.compare_prev_period,
238
+ include_raw: input.include_raw,
239
+ }),
240
+ },
241
+ apphud_analytics_subscriptions_summary: {
242
+ description: "Get subscriptions KPI summary",
243
+ inputShape: {
244
+ ...baseInputShape,
245
+ app_id: z.string().min(1).optional(),
246
+ apphud_app_id: z.string().min(1).optional(),
247
+ from: z.string().datetime().optional(),
248
+ to: z.string().datetime().optional(),
249
+ platform: analyticsPlatformSchema.optional(),
250
+ filters: analyticsFiltersSchema,
251
+ include_raw: z.boolean().optional(),
252
+ },
253
+ appIdSelector: appIdFromInput,
254
+ execute: async (container, auth, input) => container.analyticsService.subscriptionsSummary(auth, {
255
+ app_id: input.app_id,
256
+ apphud_app_id: input.apphud_app_id,
257
+ from: input.from,
258
+ to: input.to,
259
+ platform: input.platform,
260
+ filters: input.filters,
261
+ include_raw: input.include_raw,
262
+ }),
263
+ },
264
+ apphud_analytics_conversion_trial_to_paid: {
265
+ description: "Get trial to paid conversion summary",
266
+ inputShape: {
267
+ ...baseInputShape,
268
+ app_id: z.string().min(1).optional(),
269
+ apphud_app_id: z.string().min(1).optional(),
270
+ from: z.string().datetime(),
271
+ to: z.string().datetime(),
272
+ platform: analyticsPlatformSchema.optional(),
273
+ filters: analyticsFiltersSchema,
274
+ include_raw: z.boolean().optional(),
275
+ },
276
+ appIdSelector: appIdFromInput,
277
+ execute: async (container, auth, input) => container.analyticsService.conversionTrialToPaid(auth, {
278
+ app_id: input.app_id,
279
+ apphud_app_id: input.apphud_app_id,
280
+ from: input.from,
281
+ to: input.to,
282
+ platform: input.platform,
283
+ filters: input.filters,
284
+ include_raw: input.include_raw,
285
+ }),
286
+ },
287
+ apphud_analytics_cohorts_retention: {
288
+ description: "Get cohorts retention from analytics",
289
+ inputShape: {
290
+ ...baseInputShape,
291
+ app_id: z.string().min(1).optional(),
292
+ apphud_app_id: z.string().min(1).optional(),
293
+ from: z.string().datetime(),
294
+ to: z.string().datetime(),
295
+ granularity: z.enum(["day", "week"]).optional(),
296
+ max_periods: z.number().int().min(1).max(104).optional(),
297
+ platform: analyticsPlatformSchema.optional(),
298
+ filters: analyticsFiltersSchema,
299
+ include_raw: z.boolean().optional(),
300
+ },
301
+ appIdSelector: appIdFromInput,
302
+ execute: async (container, auth, input) => container.analyticsService.cohortsRetention(auth, {
303
+ app_id: input.app_id,
304
+ apphud_app_id: input.apphud_app_id,
305
+ from: input.from,
306
+ to: input.to,
307
+ granularity: input.granularity,
308
+ max_periods: input.max_periods,
309
+ platform: input.platform,
310
+ filters: input.filters,
311
+ include_raw: input.include_raw,
312
+ }),
313
+ },
314
+ apphud_analytics_cohorts_ltv: {
315
+ description: "Get cohorts cumulative LTV from analytics",
316
+ inputShape: {
317
+ ...baseInputShape,
318
+ app_id: z.string().min(1).optional(),
319
+ apphud_app_id: z.string().min(1).optional(),
320
+ from: z.string().datetime(),
321
+ to: z.string().datetime(),
322
+ granularity: z.enum(["day", "week"]).optional(),
323
+ max_periods: z.number().int().min(1).max(104).optional(),
324
+ platform: analyticsPlatformSchema.optional(),
325
+ filters: analyticsFiltersSchema,
326
+ include_raw: z.boolean().optional(),
327
+ },
328
+ appIdSelector: appIdFromInput,
329
+ execute: async (container, auth, input) => container.analyticsService.cohortsLtv(auth, {
330
+ app_id: input.app_id,
331
+ apphud_app_id: input.apphud_app_id,
332
+ from: input.from,
333
+ to: input.to,
334
+ granularity: input.granularity,
335
+ max_periods: input.max_periods,
336
+ platform: input.platform,
337
+ filters: input.filters,
338
+ include_raw: input.include_raw,
339
+ }),
340
+ },
341
+ apphud_analytics_query_raw: {
342
+ description: "Low-level analytics query tool (value/timeseries/breakdown/raw)",
343
+ inputShape: {
344
+ ...baseInputShape,
345
+ app_id: z.string().min(1).optional(),
346
+ apphud_app_id: z.string().min(1).optional(),
347
+ query: z.object({
348
+ shape: z.enum(["value", "timeseries", "breakdown", "raw"]),
349
+ metric_key: z.string().optional(),
350
+ from: z.string().datetime().optional(),
351
+ to: z.string().datetime().optional(),
352
+ granularity: z.enum(["day", "week"]).optional(),
353
+ dimension: z.string().optional(),
354
+ endpoint_path: z.string().optional(),
355
+ method: z.enum(["GET", "POST"]).optional(),
356
+ query_params: z.record(z.string()).optional(),
357
+ body: z.record(z.unknown()).optional(),
358
+ filters: analyticsFiltersSchema,
359
+ platform: analyticsPlatformSchema.optional(),
360
+ limit: z.number().int().min(1).max(500).optional(),
361
+ }),
362
+ include_raw: z.boolean().optional(),
363
+ },
364
+ appIdSelector: appIdFromInput,
365
+ execute: async (container, auth, input) => container.analyticsService.queryRaw(auth, {
366
+ app_id: input.app_id,
367
+ apphud_app_id: input.apphud_app_id,
368
+ query: input.query,
369
+ include_raw: input.include_raw,
370
+ }),
371
+ },
372
+ };
373
+ export function getRemoteToolEntries() {
374
+ return Object.entries(REMOTE_TOOL_DEFINITIONS);
375
+ }
376
+ export function isRemoteToolName(toolName) {
377
+ return REMOTE_TOOL_NAME_SET.has(toolName);
378
+ }
379
+ export async function executeRemoteTool(container, toolName, rawInput) {
380
+ const definition = REMOTE_TOOL_DEFINITIONS[toolName];
381
+ const parseResult = z.object(definition.inputShape).safeParse(rawInput ?? {});
382
+ if (!parseResult.success) {
383
+ throw new ApphudMcpError("INVALID_PAYLOAD", "Tool input validation failed", {
384
+ statusCode: 400,
385
+ details: {
386
+ tool_name: toolName,
387
+ issues: parseResult.error.issues.map((issue) => ({
388
+ path: issue.path.join("."),
389
+ message: issue.message,
390
+ })),
391
+ },
392
+ });
393
+ }
394
+ const input = parseResult.data;
395
+ const auth = resolveAuth(input, container.config);
396
+ const appId = definition.appIdSelector?.(input);
397
+ return container.toolGuard.run(auth, toolName, async () => definition.execute(container, auth, input), appId ? { appId } : undefined);
398
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apphud-mcp",
3
- "version": "0.2.2",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "description": "MCP server for Apphud dashboard analytics",
6
6
  "license": "MIT",