apphud-mcp 0.2.4 → 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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # apphud-mcp <img src="./assets/apphud-mcp-logo.svg" alt="apphud + mcp" height="28" />
2
2
 
3
- MCP server for Apphud analytics with storage-based ETL ingestion (GCS/S3) and local SQLite query cache.
3
+ MCP server for Apphud dashboard analytics endpoints (apps, events, metrics, cohorts).
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -14,10 +14,7 @@ MCP server for Apphud analytics with storage-based ETL ingestion (GCS/S3) and lo
14
14
  "args": ["-y", "apphud-mcp@0.2.2", "start"],
15
15
  "env": {
16
16
  "login": "your@apphud.email",
17
- "password": "your_apphud_password",
18
- "ETL_SOURCE": "gcs",
19
- "ETL_GCS_BUCKET": "your-bucket",
20
- "ETL_GCS_PREFIX": "apphud/exports"
17
+ "password": "your_apphud_password"
21
18
  }
22
19
  }
23
20
  }
@@ -25,10 +22,30 @@ MCP server for Apphud analytics with storage-based ETL ingestion (GCS/S3) and lo
25
22
  ```
26
23
 
27
24
  2. Restart MCP server.
28
- 3. Check ingestion:
29
- - `apphud_etl_local_status`
30
- - `apphud_apps_list_local`
31
- - `apphud_dashboard_local`
25
+ 3. Check dashboard access:
26
+ - `apphud_apps_list`
27
+ - `apphud_analytics_events_list` (requires `from`/`to`)
28
+ - `apphud_analytics_metric_timeseries` (requires `metric_key`, `from`, `to`)
29
+
30
+ ## HTTP Tool Calls
31
+
32
+ If `HTTP_ENABLED=true`, you can call tools over HTTP (stable payload path for web clients):
33
+
34
+ ```bash
35
+ curl -sS -X POST http://localhost:8080/tools/call \
36
+ -H 'content-type: application/json' \
37
+ -d '{
38
+ "name":"apphud_analytics_metric_timeseries",
39
+ "arguments":{
40
+ "app_id":"your_app_id",
41
+ "metric_key":"revenue_gross",
42
+ "from":"2026-02-01T00:00:00.000Z",
43
+ "to":"2026-02-21T23:59:59.000Z"
44
+ }
45
+ }'
46
+ ```
47
+
48
+ The endpoint also accepts `toolName` and stringified `arguments` JSON for compatibility with buggy wrappers.
32
49
 
33
50
  ## ETL Setup
34
51
 
@@ -83,11 +100,18 @@ MCP server for Apphud analytics with storage-based ETL ingestion (GCS/S3) and lo
83
100
 
84
101
  ## Useful Tools
85
102
 
86
- - `apphud_etl_local_status`
87
- - `apphud_dashboard_local`
88
- - `apphud_apps_list_local`
89
- - `apphud_analytics_*_local`
90
- - `apphud_analytics_*` (remote analytics endpoints)
103
+ - `apphud_apps_list`
104
+ - `apphud_analytics_events_list`
105
+ - `apphud_analytics_metrics_list`
106
+ - `apphud_analytics_metric_value`
107
+ - `apphud_analytics_metric_timeseries`
108
+ - `apphud_analytics_metric_breakdown`
109
+ - `apphud_analytics_revenue_summary`
110
+ - `apphud_analytics_subscriptions_summary`
111
+ - `apphud_analytics_conversion_trial_to_paid`
112
+ - `apphud_analytics_cohorts_retention`
113
+ - `apphud_analytics_cohorts_ltv`
114
+ - `apphud_analytics_query_raw`
91
115
 
92
116
  ## Development
93
117
 
package/dist/src/cli.js CHANGED
@@ -40,10 +40,10 @@ Zero-config quick start:
40
40
  2) Set env vars:
41
41
  login=<your apphud email>
42
42
  password=<your apphud password>
43
- 3) Use local-first tools:
44
- apphud_etl_local_status
45
- apphud_dashboard_local
46
- apphud_apps_list_local
43
+ 3) Use analytics tools:
44
+ apphud_apps_list
45
+ apphud_analytics_events_list
46
+ apphud_analytics_metric_timeseries
47
47
 
48
48
  Defaults:
49
49
  - SQLite DB: .apphud-mcp/apphud.db
@@ -1,100 +1,23 @@
1
+ const ANALYTICS_TOOL_PERMISSIONS = [
2
+ "apphud_apps_list",
3
+ "apphud_analytics_events_list",
4
+ "apphud_analytics_active_subscriptions",
5
+ "apphud_analytics_capabilities_get",
6
+ "apphud_analytics_metrics_list",
7
+ "apphud_analytics_metric_value",
8
+ "apphud_analytics_metric_timeseries",
9
+ "apphud_analytics_metric_breakdown",
10
+ "apphud_analytics_revenue_summary",
11
+ "apphud_analytics_subscriptions_summary",
12
+ "apphud_analytics_conversion_trial_to_paid",
13
+ "apphud_analytics_cohorts_retention",
14
+ "apphud_analytics_cohorts_ltv",
15
+ "apphud_analytics_query_raw",
16
+ ];
1
17
  export const TOOL_PERMISSIONS = {
2
- analyst: new Set([
3
- "apphud_apps_list",
4
- "apphud_apps_list_local",
5
- "apphud_dashboard_local",
6
- "apphud_analytics_events_list",
7
- "apphud_analytics_events_list_local",
8
- "apphud_analytics_active_subscriptions",
9
- "apphud_analytics_active_subscriptions_local",
10
- "apphud_analytics_capabilities_get",
11
- "apphud_analytics_capabilities_get_local",
12
- "apphud_analytics_metrics_list",
13
- "apphud_analytics_metrics_list_local",
14
- "apphud_analytics_metric_value",
15
- "apphud_analytics_metric_value_local",
16
- "apphud_analytics_metric_timeseries",
17
- "apphud_analytics_metric_timeseries_local",
18
- "apphud_analytics_metric_breakdown",
19
- "apphud_analytics_metric_breakdown_local",
20
- "apphud_analytics_revenue_summary",
21
- "apphud_analytics_revenue_summary_local",
22
- "apphud_analytics_subscriptions_summary",
23
- "apphud_analytics_subscriptions_summary_local",
24
- "apphud_analytics_conversion_trial_to_paid",
25
- "apphud_analytics_conversion_trial_to_paid_local",
26
- "apphud_analytics_cohorts_retention",
27
- "apphud_analytics_cohorts_retention_local",
28
- "apphud_analytics_cohorts_ltv",
29
- "apphud_analytics_cohorts_ltv_local",
30
- "apphud_analytics_query_raw",
31
- "apphud_analytics_query_raw_local",
32
- "apphud_etl_local_status",
33
- ]),
34
- support: new Set([
35
- "apphud_apps_list",
36
- "apphud_apps_list_local",
37
- "apphud_dashboard_local",
38
- "apphud_analytics_events_list",
39
- "apphud_analytics_events_list_local",
40
- "apphud_analytics_active_subscriptions",
41
- "apphud_analytics_active_subscriptions_local",
42
- "apphud_analytics_capabilities_get",
43
- "apphud_analytics_capabilities_get_local",
44
- "apphud_analytics_metrics_list",
45
- "apphud_analytics_metrics_list_local",
46
- "apphud_analytics_metric_value",
47
- "apphud_analytics_metric_value_local",
48
- "apphud_analytics_metric_timeseries",
49
- "apphud_analytics_metric_timeseries_local",
50
- "apphud_analytics_metric_breakdown",
51
- "apphud_analytics_metric_breakdown_local",
52
- "apphud_analytics_revenue_summary",
53
- "apphud_analytics_revenue_summary_local",
54
- "apphud_analytics_subscriptions_summary",
55
- "apphud_analytics_subscriptions_summary_local",
56
- "apphud_analytics_conversion_trial_to_paid",
57
- "apphud_analytics_conversion_trial_to_paid_local",
58
- "apphud_analytics_cohorts_retention",
59
- "apphud_analytics_cohorts_retention_local",
60
- "apphud_analytics_cohorts_ltv",
61
- "apphud_analytics_cohorts_ltv_local",
62
- "apphud_analytics_query_raw",
63
- "apphud_analytics_query_raw_local",
64
- "apphud_etl_local_status",
65
- ]),
66
- admin: new Set([
67
- "apphud_apps_list",
68
- "apphud_apps_list_local",
69
- "apphud_dashboard_local",
70
- "apphud_analytics_events_list",
71
- "apphud_analytics_events_list_local",
72
- "apphud_analytics_active_subscriptions",
73
- "apphud_analytics_active_subscriptions_local",
74
- "apphud_analytics_capabilities_get",
75
- "apphud_analytics_capabilities_get_local",
76
- "apphud_analytics_metrics_list",
77
- "apphud_analytics_metrics_list_local",
78
- "apphud_analytics_metric_value",
79
- "apphud_analytics_metric_value_local",
80
- "apphud_analytics_metric_timeseries",
81
- "apphud_analytics_metric_timeseries_local",
82
- "apphud_analytics_metric_breakdown",
83
- "apphud_analytics_metric_breakdown_local",
84
- "apphud_analytics_revenue_summary",
85
- "apphud_analytics_revenue_summary_local",
86
- "apphud_analytics_subscriptions_summary",
87
- "apphud_analytics_subscriptions_summary_local",
88
- "apphud_analytics_conversion_trial_to_paid",
89
- "apphud_analytics_conversion_trial_to_paid_local",
90
- "apphud_analytics_cohorts_retention",
91
- "apphud_analytics_cohorts_retention_local",
92
- "apphud_analytics_cohorts_ltv",
93
- "apphud_analytics_cohorts_ltv_local",
94
- "apphud_analytics_query_raw",
95
- "apphud_analytics_query_raw_local",
96
- "apphud_etl_local_status",
97
- ]),
18
+ analyst: new Set(ANALYTICS_TOOL_PERMISSIONS),
19
+ support: new Set(ANALYTICS_TOOL_PERMISSIONS),
20
+ admin: new Set(ANALYTICS_TOOL_PERMISSIONS),
98
21
  };
99
22
  export const METRIC_REVENUE_EVENT_TYPES = [
100
23
  "trial_converted",
@@ -1,11 +1,59 @@
1
1
  import express from "express";
2
- import { toToolError } from "../errors/toolError.js";
2
+ import { ApphudMcpError, isApphudMcpError, toToolError } from "../errors/toolError.js";
3
+ import { executeRemoteTool, isRemoteToolName, REMOTE_TOOL_NAMES } from "../tools/remoteTools.js";
3
4
  function sendError(res, error) {
5
+ if (isApphudMcpError(error)) {
6
+ res.status(error.statusCode).json(toToolError(error));
7
+ return;
8
+ }
4
9
  res.status(500).json(toToolError(error));
5
10
  }
11
+ export function normalizeToolCallPayload(payload) {
12
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
13
+ throw new ApphudMcpError("INVALID_PAYLOAD", "Request body must be an object", { statusCode: 400 });
14
+ }
15
+ const body = payload;
16
+ const params = body.params;
17
+ const jsonRpcParams = params && typeof params === "object" && !Array.isArray(params) ? params : undefined;
18
+ const toolNameRaw = body.name ??
19
+ body.tool ??
20
+ body.toolName ??
21
+ jsonRpcParams?.name ??
22
+ jsonRpcParams?.tool ??
23
+ jsonRpcParams?.toolName;
24
+ if (typeof toolNameRaw !== "string" || toolNameRaw.trim().length === 0) {
25
+ throw new ApphudMcpError("INVALID_PAYLOAD", "tool name is required (name or toolName)", { statusCode: 400 });
26
+ }
27
+ const inputRaw = body.arguments ??
28
+ body.payload ??
29
+ body.input ??
30
+ jsonRpcParams?.arguments ??
31
+ jsonRpcParams?.payload ??
32
+ jsonRpcParams?.input;
33
+ if (inputRaw === undefined || inputRaw === null) {
34
+ return { toolName: toolNameRaw.trim(), input: {} };
35
+ }
36
+ if (typeof inputRaw === "string") {
37
+ try {
38
+ const parsed = JSON.parse(inputRaw);
39
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
40
+ throw new Error("arguments string must decode to object");
41
+ }
42
+ return { toolName: toolNameRaw.trim(), input: parsed };
43
+ }
44
+ catch {
45
+ throw new ApphudMcpError("INVALID_PAYLOAD", "arguments string must be valid JSON object", { statusCode: 400 });
46
+ }
47
+ }
48
+ if (!inputRaw || typeof inputRaw !== "object" || Array.isArray(inputRaw)) {
49
+ throw new ApphudMcpError("INVALID_PAYLOAD", "arguments must be an object", { statusCode: 400 });
50
+ }
51
+ return { toolName: toolNameRaw.trim(), input: inputRaw };
52
+ }
6
53
  export function createHttpServer(container) {
7
54
  const app = express();
8
55
  app.disable("x-powered-by");
56
+ app.use(express.json({ limit: "1mb" }));
9
57
  app.get("/health", async (_req, res) => {
10
58
  res.status(200).json({ status: "ok" });
11
59
  });
@@ -17,7 +65,39 @@ export function createHttpServer(container) {
17
65
  timestamp: new Date().toISOString(),
18
66
  });
19
67
  });
68
+ async function handleToolCall(req, res) {
69
+ try {
70
+ const { toolName, input } = normalizeToolCallPayload(req.body);
71
+ if (!isRemoteToolName(toolName)) {
72
+ throw new ApphudMcpError("TOOL_NOT_FOUND", `Unsupported tool: ${toolName}`, {
73
+ statusCode: 404,
74
+ details: { supported_tools: REMOTE_TOOL_NAMES },
75
+ });
76
+ }
77
+ const result = await executeRemoteTool(container, toolName, input);
78
+ res.status(200).json({
79
+ ok: true,
80
+ tool: toolName,
81
+ result,
82
+ });
83
+ }
84
+ catch (error) {
85
+ sendError(res, error);
86
+ }
87
+ }
88
+ app.post("/tools/call", (req, res) => {
89
+ void handleToolCall(req, res);
90
+ });
91
+ app.post("/api/tools/call", (req, res) => {
92
+ void handleToolCall(req, res);
93
+ });
20
94
  app.use((error, _req, res, _next) => {
95
+ if (error instanceof SyntaxError) {
96
+ sendError(res, new ApphudMcpError("INVALID_PAYLOAD", "Invalid JSON body", {
97
+ statusCode: 400,
98
+ }));
99
+ return;
100
+ }
21
101
  sendError(res, error);
22
102
  });
23
103
  return app;