apphud-mcp 0.1.7 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,27 @@
1
1
  # apphud-mcp <img src="./assets/apphud-mcp-logo.svg" alt="apphud + mcp" height="28" />
2
2
 
3
- Zero-config MCP server for Apphud Dashboard Analytics.
3
+ Zero-config MCP server for Apphud Dashboard Analytics with local SQLite storage.
4
4
 
5
5
  ## Quick Start
6
6
 
7
7
  1. Optional config file (`apphud-mcp.config.json`):
8
8
 
9
9
  ```json
10
- {}
10
+ {
11
+ "storage": {
12
+ "backend": "sqlite",
13
+ "sqlite_path": ".apphud-mcp/apphud.db"
14
+ },
15
+ "etl": {
16
+ "enabled": true,
17
+ "tenant_id": "default",
18
+ "poll_interval_minutes": 60,
19
+ "storage_dir": ".apphud-etl",
20
+ "incoming_dir": ".apphud-etl/incoming",
21
+ "alerts_stale_hours": 30,
22
+ "remote_fetch_enabled": false
23
+ }
24
+ }
11
25
  ```
12
26
 
13
27
  2. Cursor MCP config:
@@ -37,10 +51,10 @@ Zero-config MCP server for Apphud Dashboard Analytics.
37
51
  ## What Changed
38
52
 
39
53
  - Default mode is analytics-only.
40
- - No Apphud app API keys required for basic usage.
41
- - No Postgres required.
54
+ - No Postgres required (SQLite is default).
42
55
  - No webhook/event-store setup required.
43
56
  - Apps are fetched directly from Apphud Dashboard API.
57
+ - ETL worker can run hourly and auto-sync app API keys from Dashboard (`settings/general` fallback).
44
58
 
45
59
  ## Basic Checks
46
60
 
@@ -52,6 +66,25 @@ Ask MCP to call:
52
66
 
53
67
  ## Available Tools
54
68
 
69
+ Local-first tools (higher priority):
70
+ - `apphud_etl_local_status`
71
+ - `apphud_apps_list_local`
72
+ - `apphud_dashboard_local`
73
+ - `apphud_analytics_events_list_local`
74
+ - `apphud_analytics_active_subscriptions_local`
75
+ - `apphud_analytics_capabilities_get_local`
76
+ - `apphud_analytics_metrics_list_local`
77
+ - `apphud_analytics_metric_value_local`
78
+ - `apphud_analytics_metric_timeseries_local`
79
+ - `apphud_analytics_metric_breakdown_local`
80
+ - `apphud_analytics_revenue_summary_local`
81
+ - `apphud_analytics_subscriptions_summary_local`
82
+ - `apphud_analytics_conversion_trial_to_paid_local`
83
+ - `apphud_analytics_cohorts_retention_local`
84
+ - `apphud_analytics_cohorts_ltv_local`
85
+ - `apphud_analytics_query_raw_local`
86
+
87
+ Remote tools (still available):
55
88
  - `apphud_apps_list`
56
89
  - `apphud_analytics_events_list`
57
90
  - `apphud_analytics_active_subscriptions`
package/dist/src/app.js CHANGED
@@ -4,6 +4,7 @@ import { RateLimiter } from "./security/rateLimiter.js";
4
4
  import { AppService } from "./services/appService.js";
5
5
  import { AnalyticsService } from "./services/analyticsService.js";
6
6
  import { ApphudClient } from "./services/apphudClient.js";
7
+ import { EtlService } from "./services/etlService.js";
7
8
  import { AuditService } from "./services/auditService.js";
8
9
  import { ToolGuard } from "./services/toolGuard.js";
9
10
  export function createServiceContainer(options) {
@@ -12,6 +13,7 @@ export function createServiceContainer(options) {
12
13
  const appService = new AppService();
13
14
  const apphudClient = new ApphudClient(config, secretStore);
14
15
  const analyticsService = new AnalyticsService(appService, apphudClient);
16
+ const etlService = new EtlService(config, apphudClient);
15
17
  const auditService = new AuditService(config);
16
18
  const rateLimiter = new RateLimiter(config.rateLimitPerMinute);
17
19
  const toolGuard = new ToolGuard(rateLimiter, auditService);
@@ -21,11 +23,14 @@ export function createServiceContainer(options) {
21
23
  appService,
22
24
  apphudClient,
23
25
  analyticsService,
26
+ etlService,
24
27
  auditService,
25
28
  toolGuard,
26
- close: async () => Promise.resolve(),
29
+ close: async () => {
30
+ await etlService.close();
31
+ },
27
32
  };
28
33
  }
29
- export async function initializeServiceContainer(_container) {
30
- return Promise.resolve();
34
+ export async function initializeServiceContainer(container) {
35
+ await container.etlService.start();
31
36
  }
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import "dotenv/config";
4
4
  const VALID_ROLES = new Set(["analyst", "support", "admin"]);
5
- const VALID_STORAGE_BACKENDS = new Set(["postgres", "memory"]);
5
+ const VALID_STORAGE_BACKENDS = new Set(["sqlite", "postgres", "memory"]);
6
6
  const VALID_APP_STATUSES = new Set(["active", "disabled"]);
7
7
  function parseNumber(value, fallback) {
8
8
  if (value === undefined || value === null || value === "") {
@@ -148,7 +148,7 @@ export function loadEnvConfig(options = {}) {
148
148
  const databaseUrlRaw = simpleConfig.database_url ?? process.env.DATABASE_URL;
149
149
  const databaseUrl = typeof databaseUrlRaw === "string" && databaseUrlRaw.trim().length > 0 ? databaseUrlRaw : undefined;
150
150
  const parsedStorageBackend = parseStorageBackend(simpleConfig.storage?.backend) ?? parseStorageBackend(process.env.STORAGE_BACKEND);
151
- const storageBackend = parsedStorageBackend ?? (databaseUrl ? "postgres" : "memory");
151
+ const storageBackend = parsedStorageBackend ?? (databaseUrl ? "postgres" : "sqlite");
152
152
  if (storageBackend === "postgres" && !databaseUrl) {
153
153
  throw new Error("DATABASE_URL is required for postgres storage backend. Set database_url or switch to storage.backend=memory");
154
154
  }
@@ -178,11 +178,36 @@ export function loadEnvConfig(options = {}) {
178
178
  const analyticsLoginPasswordSecretRef = typeof analyticsLoginPasswordSecretRefRaw === "string" && analyticsLoginPasswordSecretRefRaw.trim().length > 0
179
179
  ? analyticsLoginPasswordSecretRefRaw
180
180
  : "password";
181
+ const sqlitePathRaw = simpleConfig.storage?.sqlite_path ?? process.env.SQLITE_PATH;
182
+ const sqlitePath = typeof sqlitePathRaw === "string" && sqlitePathRaw.trim().length > 0 ? sqlitePathRaw.trim() : ".apphud-mcp/apphud.db";
183
+ const etlStorageDirRaw = simpleConfig.etl?.storage_dir ?? process.env.ETL_STORAGE_DIR;
184
+ const etlStorageDir = typeof etlStorageDirRaw === "string" && etlStorageDirRaw.trim().length > 0
185
+ ? etlStorageDirRaw.trim()
186
+ : ".apphud-etl";
187
+ const etlIncomingDirRaw = simpleConfig.etl?.incoming_dir ?? process.env.ETL_INCOMING_DIR;
188
+ const etlIncomingDir = typeof etlIncomingDirRaw === "string" && etlIncomingDirRaw.trim().length > 0
189
+ ? etlIncomingDirRaw.trim()
190
+ : `${etlStorageDir}/incoming`;
191
+ const etlTenantIdRaw = simpleConfig.etl?.tenant_id ?? process.env.ETL_TENANT_ID;
192
+ const etlTenantId = typeof etlTenantIdRaw === "string" && etlTenantIdRaw.trim().length > 0 ? etlTenantIdRaw.trim() : "default";
193
+ const etlExportsApiBaseUrlRaw = simpleConfig.apphud?.etl_exports_api_base_url ?? process.env.APPHUD_ETL_EXPORTS_API_BASE_URL;
194
+ const apphudEtlExportsApiBaseUrl = typeof etlExportsApiBaseUrlRaw === "string" && etlExportsApiBaseUrlRaw.trim().length > 0
195
+ ? etlExportsApiBaseUrlRaw.trim()
196
+ : "https://api.apphud.com/v1";
197
+ const apphudEtlExportsListPathRaw = simpleConfig.apphud?.etl_exports_list_path ?? process.env.APPHUD_ETL_EXPORTS_LIST_PATH;
198
+ const apphudEtlExportsListPath = typeof apphudEtlExportsListPathRaw === "string" && apphudEtlExportsListPathRaw.trim().length > 0
199
+ ? apphudEtlExportsListPathRaw.trim()
200
+ : "/apps/{app_id}/etl-exports";
201
+ const apphudEtlExportsDownloadPathRaw = simpleConfig.apphud?.etl_exports_download_path ?? process.env.APPHUD_ETL_EXPORTS_DOWNLOAD_PATH;
202
+ const apphudEtlExportsDownloadPath = typeof apphudEtlExportsDownloadPathRaw === "string" && apphudEtlExportsDownloadPathRaw.trim().length > 0
203
+ ? apphudEtlExportsDownloadPathRaw.trim()
204
+ : "/apps/{app_id}/etl-exports/{export_id}";
181
205
  return {
182
206
  nodeEnv: simpleConfig.node_env ?? process.env.NODE_ENV ?? "development",
183
207
  port: parseNumber(simpleConfig.transport?.port ?? process.env.PORT, 8080),
184
208
  databaseUrl,
185
209
  storageBackend,
210
+ sqlitePath,
186
211
  bootstrapApps,
187
212
  defaultTenantId,
188
213
  defaultRole,
@@ -209,7 +234,17 @@ export function loadEnvConfig(options = {}) {
209
234
  "/sessions",
210
235
  apphudAnalyticsLoginEmailSecretRef: analyticsLoginEmailSecretRef,
211
236
  apphudAnalyticsLoginPasswordSecretRef: analyticsLoginPasswordSecretRef,
237
+ apphudEtlExportsApiBaseUrl,
238
+ apphudEtlExportsListPath,
239
+ apphudEtlExportsDownloadPath,
212
240
  apphudWebhookTokenHeader: simpleConfig.security?.webhook_token_header ?? process.env.APPHUD_WEBHOOK_TOKEN_HEADER ?? "x-apphud-token",
241
+ etlEnabled: parseBoolean(simpleConfig.etl?.enabled ?? process.env.ETL_ENABLED, false),
242
+ etlTenantId,
243
+ etlPollIntervalMinutes: parseNumber(simpleConfig.etl?.poll_interval_minutes ?? process.env.ETL_POLL_INTERVAL_MINUTES, 60),
244
+ etlStorageDir,
245
+ etlIncomingDir,
246
+ etlAlertsStaleHours: parseNumber(simpleConfig.etl?.alerts_stale_hours ?? process.env.ETL_ALERTS_STALE_HOURS, 30),
247
+ etlRemoteFetchEnabled: parseBoolean(simpleConfig.etl?.remote_fetch_enabled ?? process.env.ETL_REMOTE_FETCH_ENABLED, false),
213
248
  httpEnabled: parseBoolean(simpleConfig.transport?.http_enabled ?? process.env.HTTP_ENABLED, false),
214
249
  mcpStdioEnabled: parseBoolean(simpleConfig.transport?.mcp_stdio_enabled ?? process.env.MCP_STDIO_ENABLED, true),
215
250
  };
@@ -217,7 +252,17 @@ export function loadEnvConfig(options = {}) {
217
252
  export function buildExampleSimpleConfig() {
218
253
  return {
219
254
  storage: {
220
- backend: "memory",
255
+ backend: "sqlite",
256
+ sqlite_path: ".apphud-mcp/apphud.db",
257
+ },
258
+ etl: {
259
+ enabled: false,
260
+ tenant_id: "default",
261
+ poll_interval_minutes: 60,
262
+ storage_dir: ".apphud-etl",
263
+ incoming_dir: ".apphud-etl/incoming",
264
+ alerts_stale_hours: 30,
265
+ remote_fetch_enabled: false,
221
266
  },
222
267
  defaults: {
223
268
  tenant_id: "tenant_default",
@@ -244,6 +289,9 @@ export function buildExampleSimpleConfig() {
244
289
  analytics_login_path: "/sessions",
245
290
  analytics_login_email_secret_ref: "login",
246
291
  analytics_login_password_secret_ref: "password",
292
+ etl_exports_api_base_url: "https://api.apphud.com/v1",
293
+ etl_exports_list_path: "/apps/{app_id}/etl-exports",
294
+ etl_exports_download_path: "/apps/{app_id}/etl-exports/{export_id}",
247
295
  },
248
296
  };
249
297
  }
@@ -1,51 +1,99 @@
1
1
  export const TOOL_PERMISSIONS = {
2
2
  analyst: new Set([
3
3
  "apphud_apps_list",
4
+ "apphud_apps_list_local",
5
+ "apphud_dashboard_local",
4
6
  "apphud_analytics_events_list",
7
+ "apphud_analytics_events_list_local",
5
8
  "apphud_analytics_active_subscriptions",
9
+ "apphud_analytics_active_subscriptions_local",
6
10
  "apphud_analytics_capabilities_get",
11
+ "apphud_analytics_capabilities_get_local",
7
12
  "apphud_analytics_metrics_list",
13
+ "apphud_analytics_metrics_list_local",
8
14
  "apphud_analytics_metric_value",
15
+ "apphud_analytics_metric_value_local",
9
16
  "apphud_analytics_metric_timeseries",
17
+ "apphud_analytics_metric_timeseries_local",
10
18
  "apphud_analytics_metric_breakdown",
19
+ "apphud_analytics_metric_breakdown_local",
11
20
  "apphud_analytics_revenue_summary",
21
+ "apphud_analytics_revenue_summary_local",
12
22
  "apphud_analytics_subscriptions_summary",
23
+ "apphud_analytics_subscriptions_summary_local",
13
24
  "apphud_analytics_conversion_trial_to_paid",
25
+ "apphud_analytics_conversion_trial_to_paid_local",
14
26
  "apphud_analytics_cohorts_retention",
27
+ "apphud_analytics_cohorts_retention_local",
15
28
  "apphud_analytics_cohorts_ltv",
29
+ "apphud_analytics_cohorts_ltv_local",
16
30
  "apphud_analytics_query_raw",
31
+ "apphud_analytics_query_raw_local",
32
+ "apphud_etl_local_status",
17
33
  ]),
18
34
  support: new Set([
19
35
  "apphud_apps_list",
36
+ "apphud_apps_list_local",
37
+ "apphud_dashboard_local",
20
38
  "apphud_analytics_events_list",
39
+ "apphud_analytics_events_list_local",
21
40
  "apphud_analytics_active_subscriptions",
41
+ "apphud_analytics_active_subscriptions_local",
22
42
  "apphud_analytics_capabilities_get",
43
+ "apphud_analytics_capabilities_get_local",
23
44
  "apphud_analytics_metrics_list",
45
+ "apphud_analytics_metrics_list_local",
24
46
  "apphud_analytics_metric_value",
47
+ "apphud_analytics_metric_value_local",
25
48
  "apphud_analytics_metric_timeseries",
49
+ "apphud_analytics_metric_timeseries_local",
26
50
  "apphud_analytics_metric_breakdown",
51
+ "apphud_analytics_metric_breakdown_local",
27
52
  "apphud_analytics_revenue_summary",
53
+ "apphud_analytics_revenue_summary_local",
28
54
  "apphud_analytics_subscriptions_summary",
55
+ "apphud_analytics_subscriptions_summary_local",
29
56
  "apphud_analytics_conversion_trial_to_paid",
57
+ "apphud_analytics_conversion_trial_to_paid_local",
30
58
  "apphud_analytics_cohorts_retention",
59
+ "apphud_analytics_cohorts_retention_local",
31
60
  "apphud_analytics_cohorts_ltv",
61
+ "apphud_analytics_cohorts_ltv_local",
32
62
  "apphud_analytics_query_raw",
63
+ "apphud_analytics_query_raw_local",
64
+ "apphud_etl_local_status",
33
65
  ]),
34
66
  admin: new Set([
35
67
  "apphud_apps_list",
68
+ "apphud_apps_list_local",
69
+ "apphud_dashboard_local",
36
70
  "apphud_analytics_events_list",
71
+ "apphud_analytics_events_list_local",
37
72
  "apphud_analytics_active_subscriptions",
73
+ "apphud_analytics_active_subscriptions_local",
38
74
  "apphud_analytics_capabilities_get",
75
+ "apphud_analytics_capabilities_get_local",
39
76
  "apphud_analytics_metrics_list",
77
+ "apphud_analytics_metrics_list_local",
40
78
  "apphud_analytics_metric_value",
79
+ "apphud_analytics_metric_value_local",
41
80
  "apphud_analytics_metric_timeseries",
81
+ "apphud_analytics_metric_timeseries_local",
42
82
  "apphud_analytics_metric_breakdown",
83
+ "apphud_analytics_metric_breakdown_local",
43
84
  "apphud_analytics_revenue_summary",
85
+ "apphud_analytics_revenue_summary_local",
44
86
  "apphud_analytics_subscriptions_summary",
87
+ "apphud_analytics_subscriptions_summary_local",
45
88
  "apphud_analytics_conversion_trial_to_paid",
89
+ "apphud_analytics_conversion_trial_to_paid_local",
46
90
  "apphud_analytics_cohorts_retention",
91
+ "apphud_analytics_cohorts_retention_local",
47
92
  "apphud_analytics_cohorts_ltv",
93
+ "apphud_analytics_cohorts_ltv_local",
48
94
  "apphud_analytics_query_raw",
95
+ "apphud_analytics_query_raw_local",
96
+ "apphud_etl_local_status",
49
97
  ]),
50
98
  };
51
99
  export const METRIC_REVENUE_EVENT_TYPES = [