apphud-mcp 0.1.0
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/.env.example +23 -0
- package/CHANGELOG.md +21 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/CONTRIBUTING.md +73 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/SECURITY.md +25 -0
- package/SUPPORT.md +26 -0
- package/assets/apphud-mcp-logo.svg +6 -0
- package/dist/src/app.js +31 -0
- package/dist/src/cli.js +94 -0
- package/dist/src/config/env.js +249 -0
- package/dist/src/domain/constants.js +56 -0
- package/dist/src/domain/models.js +1 -0
- package/dist/src/errors/toolError.js +40 -0
- package/dist/src/http/server.js +24 -0
- package/dist/src/index.js +47 -0
- package/dist/src/mcp/server.js +435 -0
- package/dist/src/security/authResolver.js +43 -0
- package/dist/src/security/rateLimiter.js +22 -0
- package/dist/src/security/rbac.js +11 -0
- package/dist/src/security/secretStore.js +14 -0
- package/dist/src/services/analyticsService.js +934 -0
- package/dist/src/services/appService.js +15 -0
- package/dist/src/services/apphudClient.js +1632 -0
- package/dist/src/services/auditService.js +12 -0
- package/dist/src/services/toolGuard.js +30 -0
- package/package.json +61 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const TOOL_PERMISSIONS = {
|
|
2
|
+
analyst: new Set([
|
|
3
|
+
"apphud.apps.list",
|
|
4
|
+
"apphud.analytics.events.list",
|
|
5
|
+
"apphud.analytics.active_subscriptions",
|
|
6
|
+
"apphud.analytics.capabilities.get",
|
|
7
|
+
"apphud.analytics.metrics.list",
|
|
8
|
+
"apphud.analytics.metric.value",
|
|
9
|
+
"apphud.analytics.metric.timeseries",
|
|
10
|
+
"apphud.analytics.metric.breakdown",
|
|
11
|
+
"apphud.analytics.revenue.summary",
|
|
12
|
+
"apphud.analytics.subscriptions.summary",
|
|
13
|
+
"apphud.analytics.conversion.trial_to_paid",
|
|
14
|
+
"apphud.analytics.cohorts.retention",
|
|
15
|
+
"apphud.analytics.cohorts.ltv",
|
|
16
|
+
"apphud.analytics.query.raw",
|
|
17
|
+
]),
|
|
18
|
+
support: new Set([
|
|
19
|
+
"apphud.apps.list",
|
|
20
|
+
"apphud.analytics.events.list",
|
|
21
|
+
"apphud.analytics.active_subscriptions",
|
|
22
|
+
"apphud.analytics.capabilities.get",
|
|
23
|
+
"apphud.analytics.metrics.list",
|
|
24
|
+
"apphud.analytics.metric.value",
|
|
25
|
+
"apphud.analytics.metric.timeseries",
|
|
26
|
+
"apphud.analytics.metric.breakdown",
|
|
27
|
+
"apphud.analytics.revenue.summary",
|
|
28
|
+
"apphud.analytics.subscriptions.summary",
|
|
29
|
+
"apphud.analytics.conversion.trial_to_paid",
|
|
30
|
+
"apphud.analytics.cohorts.retention",
|
|
31
|
+
"apphud.analytics.cohorts.ltv",
|
|
32
|
+
"apphud.analytics.query.raw",
|
|
33
|
+
]),
|
|
34
|
+
admin: new Set([
|
|
35
|
+
"apphud.apps.list",
|
|
36
|
+
"apphud.analytics.events.list",
|
|
37
|
+
"apphud.analytics.active_subscriptions",
|
|
38
|
+
"apphud.analytics.capabilities.get",
|
|
39
|
+
"apphud.analytics.metrics.list",
|
|
40
|
+
"apphud.analytics.metric.value",
|
|
41
|
+
"apphud.analytics.metric.timeseries",
|
|
42
|
+
"apphud.analytics.metric.breakdown",
|
|
43
|
+
"apphud.analytics.revenue.summary",
|
|
44
|
+
"apphud.analytics.subscriptions.summary",
|
|
45
|
+
"apphud.analytics.conversion.trial_to_paid",
|
|
46
|
+
"apphud.analytics.cohorts.retention",
|
|
47
|
+
"apphud.analytics.cohorts.ltv",
|
|
48
|
+
"apphud.analytics.query.raw",
|
|
49
|
+
]),
|
|
50
|
+
};
|
|
51
|
+
export const METRIC_REVENUE_EVENT_TYPES = [
|
|
52
|
+
"trial_converted",
|
|
53
|
+
"subscription_started",
|
|
54
|
+
"renewal",
|
|
55
|
+
"refund",
|
|
56
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class ApphudMcpError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
details;
|
|
4
|
+
actionHint;
|
|
5
|
+
statusCode;
|
|
6
|
+
constructor(code, message, options) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ApphudMcpError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.details = options?.details;
|
|
11
|
+
this.actionHint = options?.actionHint;
|
|
12
|
+
this.statusCode = options?.statusCode ?? 500;
|
|
13
|
+
}
|
|
14
|
+
toToolError() {
|
|
15
|
+
return {
|
|
16
|
+
code: this.code,
|
|
17
|
+
message: this.message,
|
|
18
|
+
details: this.details,
|
|
19
|
+
action_hint: this.actionHint,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function isApphudMcpError(error) {
|
|
24
|
+
return error instanceof ApphudMcpError;
|
|
25
|
+
}
|
|
26
|
+
export function toToolError(error) {
|
|
27
|
+
if (isApphudMcpError(error)) {
|
|
28
|
+
return error.toToolError();
|
|
29
|
+
}
|
|
30
|
+
if (error instanceof Error) {
|
|
31
|
+
return {
|
|
32
|
+
code: "INTERNAL_ERROR",
|
|
33
|
+
message: error.message,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
code: "INTERNAL_ERROR",
|
|
38
|
+
message: "Unexpected error",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { toToolError } from "../errors/toolError.js";
|
|
3
|
+
function sendError(res, error) {
|
|
4
|
+
res.status(500).json(toToolError(error));
|
|
5
|
+
}
|
|
6
|
+
export function createHttpServer(container) {
|
|
7
|
+
const app = express();
|
|
8
|
+
app.disable("x-powered-by");
|
|
9
|
+
app.get("/health", async (_req, res) => {
|
|
10
|
+
res.status(200).json({ status: "ok" });
|
|
11
|
+
});
|
|
12
|
+
app.get("/status", async (_req, res) => {
|
|
13
|
+
res.status(200).json({
|
|
14
|
+
status: "ok",
|
|
15
|
+
mode: "analytics_only",
|
|
16
|
+
node_env: container.config.nodeEnv,
|
|
17
|
+
timestamp: new Date().toISOString(),
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
app.use((error, _req, res, _next) => {
|
|
21
|
+
sendError(res, error);
|
|
22
|
+
});
|
|
23
|
+
return app;
|
|
24
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createServiceContainer, initializeServiceContainer } from "./app.js";
|
|
2
|
+
import { loadEnvConfig } from "./config/env.js";
|
|
3
|
+
import { createHttpServer } from "./http/server.js";
|
|
4
|
+
import { startMcpStdioServer } from "./mcp/server.js";
|
|
5
|
+
export async function startRuntime(container) {
|
|
6
|
+
await initializeServiceContainer(container);
|
|
7
|
+
let httpServer;
|
|
8
|
+
if (container.config.httpEnabled) {
|
|
9
|
+
const app = createHttpServer(container);
|
|
10
|
+
await new Promise((resolve) => {
|
|
11
|
+
httpServer = app.listen(container.config.port, () => {
|
|
12
|
+
console.error(`HTTP server listening on port ${container.config.port}`);
|
|
13
|
+
resolve();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
if (container.config.mcpStdioEnabled) {
|
|
18
|
+
startMcpStdioServer(container)
|
|
19
|
+
.then(() => {
|
|
20
|
+
console.error("MCP stdio transport started");
|
|
21
|
+
})
|
|
22
|
+
.catch((error) => {
|
|
23
|
+
console.error("Failed to start MCP stdio server", error);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function shutdown() {
|
|
28
|
+
if (httpServer) {
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
httpServer?.close((error) => {
|
|
31
|
+
if (error) {
|
|
32
|
+
reject(error);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
resolve();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
await container.close();
|
|
40
|
+
}
|
|
41
|
+
return { shutdown };
|
|
42
|
+
}
|
|
43
|
+
export async function startFromConfig(options = {}) {
|
|
44
|
+
const config = loadEnvConfig(options);
|
|
45
|
+
const container = createServiceContainer({ config });
|
|
46
|
+
return startRuntime(container);
|
|
47
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { toToolError } from "../errors/toolError.js";
|
|
5
|
+
import { resolveAuth } from "../security/authResolver.js";
|
|
6
|
+
const authSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
tenant_id: z.string().min(1),
|
|
9
|
+
role: z.enum(["analyst", "support", "admin"]),
|
|
10
|
+
actor_id: z.string().min(1).optional(),
|
|
11
|
+
})
|
|
12
|
+
.optional();
|
|
13
|
+
const analyticsPlatformSchema = z.enum(["ios", "android", "web"]);
|
|
14
|
+
const analyticsFiltersSchema = z
|
|
15
|
+
.record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
|
|
16
|
+
.optional();
|
|
17
|
+
const analyticsEventsFiltersSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
user_id: z.string().min(1).optional(),
|
|
20
|
+
product_id: z.string().min(1).optional(),
|
|
21
|
+
event_type: z.string().min(1).optional(),
|
|
22
|
+
country: z.string().min(1).optional(),
|
|
23
|
+
platform: analyticsPlatformSchema.optional(),
|
|
24
|
+
})
|
|
25
|
+
.optional();
|
|
26
|
+
function jsonResult(payload, isError = false) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
29
|
+
isError: isError || undefined,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function createMcpServer(container) {
|
|
33
|
+
const server = new McpServer({
|
|
34
|
+
name: "apphud-mcp",
|
|
35
|
+
version: "0.2.0",
|
|
36
|
+
});
|
|
37
|
+
server.tool("apphud.apps.list", "List apps available in authenticated Apphud dashboard account", {
|
|
38
|
+
tenant_id: z.string().optional(),
|
|
39
|
+
auth: authSchema,
|
|
40
|
+
include_raw: z.boolean().optional(),
|
|
41
|
+
}, async (input) => {
|
|
42
|
+
const auth = resolveAuth(input, container.config);
|
|
43
|
+
try {
|
|
44
|
+
const result = await container.toolGuard.run(auth, "apphud.apps.list", async () => container.analyticsService.appsList(auth, {
|
|
45
|
+
include_raw: input.include_raw,
|
|
46
|
+
}));
|
|
47
|
+
return jsonResult(result);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return jsonResult(toToolError(error), true);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
server.tool("apphud.analytics.events.list", "List raw subscription events from Apphud dashboard event feed", {
|
|
54
|
+
tenant_id: z.string().optional(),
|
|
55
|
+
auth: authSchema,
|
|
56
|
+
app_id: z.string().min(1).optional(),
|
|
57
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
58
|
+
from: z.string().datetime(),
|
|
59
|
+
to: z.string().datetime(),
|
|
60
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
61
|
+
cursor: z.string().min(1).optional(),
|
|
62
|
+
filters: analyticsEventsFiltersSchema,
|
|
63
|
+
include_raw: z.boolean().optional(),
|
|
64
|
+
}, async (input) => {
|
|
65
|
+
const auth = resolveAuth(input, container.config);
|
|
66
|
+
try {
|
|
67
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.events.list", async () => container.analyticsService.eventsList(auth, {
|
|
68
|
+
app_id: input.app_id,
|
|
69
|
+
apphud_app_id: input.apphud_app_id,
|
|
70
|
+
from: input.from,
|
|
71
|
+
to: input.to,
|
|
72
|
+
limit: input.limit,
|
|
73
|
+
cursor: input.cursor,
|
|
74
|
+
filters: input.filters,
|
|
75
|
+
include_raw: input.include_raw,
|
|
76
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
77
|
+
return jsonResult(result);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return jsonResult(toToolError(error), true);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
server.tool("apphud.analytics.active_subscriptions", "Fetch active paid subscriptions directly from Apphud analytics endpoint", {
|
|
84
|
+
tenant_id: z.string().optional(),
|
|
85
|
+
auth: authSchema,
|
|
86
|
+
app_id: z.string().min(1).optional(),
|
|
87
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
88
|
+
platform: analyticsPlatformSchema.optional(),
|
|
89
|
+
include_raw: z.boolean().optional(),
|
|
90
|
+
}, async (input) => {
|
|
91
|
+
const auth = resolveAuth(input, container.config);
|
|
92
|
+
try {
|
|
93
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.active_subscriptions", async () => container.analyticsService.activeSubscriptions(auth, {
|
|
94
|
+
app_id: input.app_id,
|
|
95
|
+
apphud_app_id: input.apphud_app_id,
|
|
96
|
+
platform: input.platform,
|
|
97
|
+
include_raw: input.include_raw,
|
|
98
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
99
|
+
return jsonResult(result);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return jsonResult(toToolError(error), true);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
server.tool("apphud.analytics.capabilities.get", "Get analytics capabilities and available metrics", {
|
|
106
|
+
tenant_id: z.string().optional(),
|
|
107
|
+
auth: authSchema,
|
|
108
|
+
app_id: z.string().min(1).optional(),
|
|
109
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
110
|
+
include_raw: z.boolean().optional(),
|
|
111
|
+
}, async (input) => {
|
|
112
|
+
const auth = resolveAuth(input, container.config);
|
|
113
|
+
try {
|
|
114
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.capabilities.get", async () => container.analyticsService.capabilities(auth, {
|
|
115
|
+
app_id: input.app_id,
|
|
116
|
+
apphud_app_id: input.apphud_app_id,
|
|
117
|
+
include_raw: input.include_raw,
|
|
118
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
119
|
+
return jsonResult(result);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
return jsonResult(toToolError(error), true);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
server.tool("apphud.analytics.metrics.list", "List analytics metric keys", {
|
|
126
|
+
tenant_id: z.string().optional(),
|
|
127
|
+
auth: authSchema,
|
|
128
|
+
app_id: z.string().min(1).optional(),
|
|
129
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
130
|
+
include_raw: z.boolean().optional(),
|
|
131
|
+
}, async (input) => {
|
|
132
|
+
const auth = resolveAuth(input, container.config);
|
|
133
|
+
try {
|
|
134
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.metrics.list", async () => container.analyticsService.metricsList(auth, {
|
|
135
|
+
app_id: input.app_id,
|
|
136
|
+
apphud_app_id: input.apphud_app_id,
|
|
137
|
+
include_raw: input.include_raw,
|
|
138
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
139
|
+
return jsonResult(result);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return jsonResult(toToolError(error), true);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
server.tool("apphud.analytics.metric.value", "Get single metric value for period/snapshot", {
|
|
146
|
+
tenant_id: z.string().optional(),
|
|
147
|
+
auth: authSchema,
|
|
148
|
+
app_id: z.string().min(1).optional(),
|
|
149
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
150
|
+
metric_key: z.string().min(1),
|
|
151
|
+
from: z.string().datetime().optional(),
|
|
152
|
+
to: z.string().datetime().optional(),
|
|
153
|
+
platform: analyticsPlatformSchema.optional(),
|
|
154
|
+
filters: analyticsFiltersSchema,
|
|
155
|
+
include_raw: z.boolean().optional(),
|
|
156
|
+
}, async (input) => {
|
|
157
|
+
const auth = resolveAuth(input, container.config);
|
|
158
|
+
try {
|
|
159
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.metric.value", async () => container.analyticsService.metricValue(auth, {
|
|
160
|
+
app_id: input.app_id,
|
|
161
|
+
apphud_app_id: input.apphud_app_id,
|
|
162
|
+
metric_key: input.metric_key,
|
|
163
|
+
from: input.from,
|
|
164
|
+
to: input.to,
|
|
165
|
+
platform: input.platform,
|
|
166
|
+
filters: input.filters,
|
|
167
|
+
include_raw: input.include_raw,
|
|
168
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
169
|
+
return jsonResult(result);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return jsonResult(toToolError(error), true);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
server.tool("apphud.analytics.metric.timeseries", "Get metric timeseries by date range", {
|
|
176
|
+
tenant_id: z.string().optional(),
|
|
177
|
+
auth: authSchema,
|
|
178
|
+
app_id: z.string().min(1).optional(),
|
|
179
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
180
|
+
metric_key: z.string().min(1),
|
|
181
|
+
from: z.string().datetime(),
|
|
182
|
+
to: z.string().datetime(),
|
|
183
|
+
granularity: z.enum(["day", "week"]).optional(),
|
|
184
|
+
platform: analyticsPlatformSchema.optional(),
|
|
185
|
+
filters: analyticsFiltersSchema,
|
|
186
|
+
include_raw: z.boolean().optional(),
|
|
187
|
+
}, async (input) => {
|
|
188
|
+
const auth = resolveAuth(input, container.config);
|
|
189
|
+
try {
|
|
190
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.metric.timeseries", async () => container.analyticsService.metricTimeseries(auth, {
|
|
191
|
+
app_id: input.app_id,
|
|
192
|
+
apphud_app_id: input.apphud_app_id,
|
|
193
|
+
metric_key: input.metric_key,
|
|
194
|
+
from: input.from,
|
|
195
|
+
to: input.to,
|
|
196
|
+
granularity: input.granularity,
|
|
197
|
+
platform: input.platform,
|
|
198
|
+
filters: input.filters,
|
|
199
|
+
include_raw: input.include_raw,
|
|
200
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
201
|
+
return jsonResult(result);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
return jsonResult(toToolError(error), true);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
server.tool("apphud.analytics.metric.breakdown", "Get metric breakdown by dimension", {
|
|
208
|
+
tenant_id: z.string().optional(),
|
|
209
|
+
auth: authSchema,
|
|
210
|
+
app_id: z.string().min(1).optional(),
|
|
211
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
212
|
+
metric_key: z.string().min(1),
|
|
213
|
+
from: z.string().datetime(),
|
|
214
|
+
to: z.string().datetime(),
|
|
215
|
+
dimension: z.string().min(1),
|
|
216
|
+
granularity: z.enum(["day", "week"]).optional(),
|
|
217
|
+
platform: analyticsPlatformSchema.optional(),
|
|
218
|
+
filters: analyticsFiltersSchema,
|
|
219
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
220
|
+
include_raw: z.boolean().optional(),
|
|
221
|
+
}, async (input) => {
|
|
222
|
+
const auth = resolveAuth(input, container.config);
|
|
223
|
+
try {
|
|
224
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.metric.breakdown", async () => container.analyticsService.metricBreakdown(auth, {
|
|
225
|
+
app_id: input.app_id,
|
|
226
|
+
apphud_app_id: input.apphud_app_id,
|
|
227
|
+
metric_key: input.metric_key,
|
|
228
|
+
from: input.from,
|
|
229
|
+
to: input.to,
|
|
230
|
+
dimension: input.dimension,
|
|
231
|
+
granularity: input.granularity,
|
|
232
|
+
platform: input.platform,
|
|
233
|
+
filters: input.filters,
|
|
234
|
+
limit: input.limit,
|
|
235
|
+
include_raw: input.include_raw,
|
|
236
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
237
|
+
return jsonResult(result);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
return jsonResult(toToolError(error), true);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
server.tool("apphud.analytics.revenue.summary", "Get revenue summary for date range", {
|
|
244
|
+
tenant_id: z.string().optional(),
|
|
245
|
+
auth: authSchema,
|
|
246
|
+
app_id: z.string().min(1).optional(),
|
|
247
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
248
|
+
from: z.string().datetime(),
|
|
249
|
+
to: z.string().datetime(),
|
|
250
|
+
platform: analyticsPlatformSchema.optional(),
|
|
251
|
+
filters: analyticsFiltersSchema,
|
|
252
|
+
compare_prev_period: z.boolean().optional(),
|
|
253
|
+
include_raw: z.boolean().optional(),
|
|
254
|
+
}, async (input) => {
|
|
255
|
+
const auth = resolveAuth(input, container.config);
|
|
256
|
+
try {
|
|
257
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.revenue.summary", async () => container.analyticsService.revenueSummary(auth, {
|
|
258
|
+
app_id: input.app_id,
|
|
259
|
+
apphud_app_id: input.apphud_app_id,
|
|
260
|
+
from: input.from,
|
|
261
|
+
to: input.to,
|
|
262
|
+
platform: input.platform,
|
|
263
|
+
filters: input.filters,
|
|
264
|
+
compare_prev_period: input.compare_prev_period,
|
|
265
|
+
include_raw: input.include_raw,
|
|
266
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
267
|
+
return jsonResult(result);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
return jsonResult(toToolError(error), true);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
server.tool("apphud.analytics.subscriptions.summary", "Get subscriptions KPI summary", {
|
|
274
|
+
tenant_id: z.string().optional(),
|
|
275
|
+
auth: authSchema,
|
|
276
|
+
app_id: z.string().min(1).optional(),
|
|
277
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
278
|
+
from: z.string().datetime().optional(),
|
|
279
|
+
to: z.string().datetime().optional(),
|
|
280
|
+
platform: analyticsPlatformSchema.optional(),
|
|
281
|
+
filters: analyticsFiltersSchema,
|
|
282
|
+
include_raw: z.boolean().optional(),
|
|
283
|
+
}, async (input) => {
|
|
284
|
+
const auth = resolveAuth(input, container.config);
|
|
285
|
+
try {
|
|
286
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.subscriptions.summary", async () => container.analyticsService.subscriptionsSummary(auth, {
|
|
287
|
+
app_id: input.app_id,
|
|
288
|
+
apphud_app_id: input.apphud_app_id,
|
|
289
|
+
from: input.from,
|
|
290
|
+
to: input.to,
|
|
291
|
+
platform: input.platform,
|
|
292
|
+
filters: input.filters,
|
|
293
|
+
include_raw: input.include_raw,
|
|
294
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
295
|
+
return jsonResult(result);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
return jsonResult(toToolError(error), true);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
server.tool("apphud.analytics.conversion.trial_to_paid", "Get trial to paid conversion summary", {
|
|
302
|
+
tenant_id: z.string().optional(),
|
|
303
|
+
auth: authSchema,
|
|
304
|
+
app_id: z.string().min(1).optional(),
|
|
305
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
306
|
+
from: z.string().datetime(),
|
|
307
|
+
to: z.string().datetime(),
|
|
308
|
+
platform: analyticsPlatformSchema.optional(),
|
|
309
|
+
filters: analyticsFiltersSchema,
|
|
310
|
+
include_raw: z.boolean().optional(),
|
|
311
|
+
}, async (input) => {
|
|
312
|
+
const auth = resolveAuth(input, container.config);
|
|
313
|
+
try {
|
|
314
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.conversion.trial_to_paid", async () => container.analyticsService.conversionTrialToPaid(auth, {
|
|
315
|
+
app_id: input.app_id,
|
|
316
|
+
apphud_app_id: input.apphud_app_id,
|
|
317
|
+
from: input.from,
|
|
318
|
+
to: input.to,
|
|
319
|
+
platform: input.platform,
|
|
320
|
+
filters: input.filters,
|
|
321
|
+
include_raw: input.include_raw,
|
|
322
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
323
|
+
return jsonResult(result);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
return jsonResult(toToolError(error), true);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
server.tool("apphud.analytics.cohorts.retention", "Get cohorts retention from analytics", {
|
|
330
|
+
tenant_id: z.string().optional(),
|
|
331
|
+
auth: authSchema,
|
|
332
|
+
app_id: z.string().min(1).optional(),
|
|
333
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
334
|
+
from: z.string().datetime(),
|
|
335
|
+
to: z.string().datetime(),
|
|
336
|
+
granularity: z.enum(["day", "week"]).optional(),
|
|
337
|
+
max_periods: z.number().int().min(1).max(104).optional(),
|
|
338
|
+
platform: analyticsPlatformSchema.optional(),
|
|
339
|
+
filters: analyticsFiltersSchema,
|
|
340
|
+
include_raw: z.boolean().optional(),
|
|
341
|
+
}, async (input) => {
|
|
342
|
+
const auth = resolveAuth(input, container.config);
|
|
343
|
+
try {
|
|
344
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.cohorts.retention", async () => container.analyticsService.cohortsRetention(auth, {
|
|
345
|
+
app_id: input.app_id,
|
|
346
|
+
apphud_app_id: input.apphud_app_id,
|
|
347
|
+
from: input.from,
|
|
348
|
+
to: input.to,
|
|
349
|
+
granularity: input.granularity,
|
|
350
|
+
max_periods: input.max_periods,
|
|
351
|
+
platform: input.platform,
|
|
352
|
+
filters: input.filters,
|
|
353
|
+
include_raw: input.include_raw,
|
|
354
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
355
|
+
return jsonResult(result);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
return jsonResult(toToolError(error), true);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
server.tool("apphud.analytics.cohorts.ltv", "Get cohorts cumulative LTV from analytics", {
|
|
362
|
+
tenant_id: z.string().optional(),
|
|
363
|
+
auth: authSchema,
|
|
364
|
+
app_id: z.string().min(1).optional(),
|
|
365
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
366
|
+
from: z.string().datetime(),
|
|
367
|
+
to: z.string().datetime(),
|
|
368
|
+
granularity: z.enum(["day", "week"]).optional(),
|
|
369
|
+
max_periods: z.number().int().min(1).max(104).optional(),
|
|
370
|
+
platform: analyticsPlatformSchema.optional(),
|
|
371
|
+
filters: analyticsFiltersSchema,
|
|
372
|
+
include_raw: z.boolean().optional(),
|
|
373
|
+
}, async (input) => {
|
|
374
|
+
const auth = resolveAuth(input, container.config);
|
|
375
|
+
try {
|
|
376
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.cohorts.ltv", async () => container.analyticsService.cohortsLtv(auth, {
|
|
377
|
+
app_id: input.app_id,
|
|
378
|
+
apphud_app_id: input.apphud_app_id,
|
|
379
|
+
from: input.from,
|
|
380
|
+
to: input.to,
|
|
381
|
+
granularity: input.granularity,
|
|
382
|
+
max_periods: input.max_periods,
|
|
383
|
+
platform: input.platform,
|
|
384
|
+
filters: input.filters,
|
|
385
|
+
include_raw: input.include_raw,
|
|
386
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
387
|
+
return jsonResult(result);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
return jsonResult(toToolError(error), true);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
server.tool("apphud.analytics.query.raw", "Low-level analytics query tool (value/timeseries/breakdown/raw)", {
|
|
394
|
+
tenant_id: z.string().optional(),
|
|
395
|
+
auth: authSchema,
|
|
396
|
+
app_id: z.string().min(1).optional(),
|
|
397
|
+
apphud_app_id: z.string().min(1).optional(),
|
|
398
|
+
query: z.object({
|
|
399
|
+
shape: z.enum(["value", "timeseries", "breakdown", "raw"]),
|
|
400
|
+
metric_key: z.string().optional(),
|
|
401
|
+
from: z.string().datetime().optional(),
|
|
402
|
+
to: z.string().datetime().optional(),
|
|
403
|
+
granularity: z.enum(["day", "week"]).optional(),
|
|
404
|
+
dimension: z.string().optional(),
|
|
405
|
+
endpoint_path: z.string().optional(),
|
|
406
|
+
method: z.enum(["GET", "POST"]).optional(),
|
|
407
|
+
query_params: z.record(z.string()).optional(),
|
|
408
|
+
body: z.record(z.unknown()).optional(),
|
|
409
|
+
filters: analyticsFiltersSchema,
|
|
410
|
+
platform: analyticsPlatformSchema.optional(),
|
|
411
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
412
|
+
}),
|
|
413
|
+
include_raw: z.boolean().optional(),
|
|
414
|
+
}, async (input) => {
|
|
415
|
+
const auth = resolveAuth(input, container.config);
|
|
416
|
+
try {
|
|
417
|
+
const result = await container.toolGuard.run(auth, "apphud.analytics.query.raw", async () => container.analyticsService.queryRaw(auth, {
|
|
418
|
+
app_id: input.app_id,
|
|
419
|
+
apphud_app_id: input.apphud_app_id,
|
|
420
|
+
query: input.query,
|
|
421
|
+
include_raw: input.include_raw,
|
|
422
|
+
}), { appId: input.app_id ?? input.apphud_app_id });
|
|
423
|
+
return jsonResult(result);
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
return jsonResult(toToolError(error), true);
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
return server;
|
|
430
|
+
}
|
|
431
|
+
export async function startMcpStdioServer(container) {
|
|
432
|
+
const server = createMcpServer(container);
|
|
433
|
+
const transport = new StdioServerTransport();
|
|
434
|
+
await server.connect(transport);
|
|
435
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ApphudMcpError } from "../errors/toolError.js";
|
|
2
|
+
const VALID_ROLES = ["analyst", "support", "admin"];
|
|
3
|
+
export function resolveAuth(input, config) {
|
|
4
|
+
const tenantId = input?.auth?.tenant_id ?? input?.tenant_id ?? config.defaultTenantId;
|
|
5
|
+
const role = input?.auth?.role ?? config.defaultRole;
|
|
6
|
+
const actorId = input?.auth?.actor_id ?? config.defaultUserId ?? "unknown_actor";
|
|
7
|
+
if (!tenantId || !role) {
|
|
8
|
+
throw new ApphudMcpError("UNAUTHORIZED", "Auth context is required", {
|
|
9
|
+
statusCode: 401,
|
|
10
|
+
actionHint: "Pass auth.tenant_id and auth.role in tool input or configure DEFAULT_TENANT_ID/DEFAULT_ROLE",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
if (!VALID_ROLES.includes(role)) {
|
|
14
|
+
throw new ApphudMcpError("UNAUTHORIZED", `Invalid role: ${role}`, {
|
|
15
|
+
statusCode: 401,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
tenantId,
|
|
20
|
+
role,
|
|
21
|
+
actorId,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function resolveHttpAuth(headers) {
|
|
25
|
+
const tenantId = headerValue(headers["x-tenant-id"]);
|
|
26
|
+
const role = headerValue(headers["x-role"]);
|
|
27
|
+
const actorId = headerValue(headers["x-user-id"]) ?? "http_actor";
|
|
28
|
+
if (!tenantId || !role) {
|
|
29
|
+
throw new ApphudMcpError("UNAUTHORIZED", "x-tenant-id and x-role headers are required", {
|
|
30
|
+
statusCode: 401,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (!VALID_ROLES.includes(role)) {
|
|
34
|
+
throw new ApphudMcpError("UNAUTHORIZED", `Invalid role: ${role}`, { statusCode: 401 });
|
|
35
|
+
}
|
|
36
|
+
return { tenantId, role, actorId };
|
|
37
|
+
}
|
|
38
|
+
function headerValue(value) {
|
|
39
|
+
if (!value) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return Array.isArray(value) ? value[0] : value;
|
|
43
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ApphudMcpError } from "../errors/toolError.js";
|
|
2
|
+
export class RateLimiter {
|
|
3
|
+
maxPerMinute;
|
|
4
|
+
buckets = new Map();
|
|
5
|
+
constructor(maxPerMinute) {
|
|
6
|
+
this.maxPerMinute = maxPerMinute;
|
|
7
|
+
}
|
|
8
|
+
assertLimit(key) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
const minuteAgo = now - 60_000;
|
|
11
|
+
const bucket = this.buckets.get(key) ?? { hits: [] };
|
|
12
|
+
bucket.hits = bucket.hits.filter((timestamp) => timestamp >= minuteAgo);
|
|
13
|
+
if (bucket.hits.length >= this.maxPerMinute) {
|
|
14
|
+
throw new ApphudMcpError("RATE_LIMITED", "Rate limit exceeded", {
|
|
15
|
+
statusCode: 429,
|
|
16
|
+
details: { key, max_per_minute: this.maxPerMinute },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
bucket.hits.push(now);
|
|
20
|
+
this.buckets.set(key, bucket);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TOOL_PERMISSIONS } from "../domain/constants.js";
|
|
2
|
+
import { ApphudMcpError } from "../errors/toolError.js";
|
|
3
|
+
export function assertToolAccess(auth, toolName) {
|
|
4
|
+
const allowed = TOOL_PERMISSIONS[auth.role];
|
|
5
|
+
if (!allowed.has(toolName)) {
|
|
6
|
+
throw new ApphudMcpError("FORBIDDEN", `Role ${auth.role} cannot call ${toolName}`, {
|
|
7
|
+
statusCode: 403,
|
|
8
|
+
details: { role: auth.role, tool: toolName },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|