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
package/.env.example
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
NODE_ENV=development
|
|
2
|
+
|
|
3
|
+
# Required (zero-config auth)
|
|
4
|
+
login=your@apphud.email
|
|
5
|
+
password=your_apphud_password
|
|
6
|
+
|
|
7
|
+
# Optional
|
|
8
|
+
PORT=8080
|
|
9
|
+
HTTP_ENABLED=false
|
|
10
|
+
MCP_STDIO_ENABLED=true
|
|
11
|
+
|
|
12
|
+
# Optional auth context defaults
|
|
13
|
+
DEFAULT_TENANT_ID=tenant_default
|
|
14
|
+
DEFAULT_ROLE=admin
|
|
15
|
+
DEFAULT_USER_ID=local_user
|
|
16
|
+
|
|
17
|
+
# Optional analytics overrides
|
|
18
|
+
APPHUD_ANALYTICS_API_BASE_URL=https://api-legacy.apphud.com
|
|
19
|
+
APPHUD_ANALYTICS_AUTH_HEADER=Cookie
|
|
20
|
+
APPHUD_ANALYTICS_AUTH_PREFIX=
|
|
21
|
+
APPHUD_ANALYTICS_LOGIN_PATH=/sessions
|
|
22
|
+
APPHUD_ANALYTICS_LOGIN_EMAIL_SECRET_REF=login
|
|
23
|
+
APPHUD_ANALYTICS_LOGIN_PASSWORD_SECRET_REF=password
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Open source contributor documentation (`CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`)
|
|
10
|
+
- GitHub issue and PR templates
|
|
11
|
+
- CI workflow for typecheck, tests, and build
|
|
12
|
+
|
|
13
|
+
## [0.1.0] - 2026-02-16
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- MCP tools for Apphud customers, webhook ingest, events, metrics, and cohorts
|
|
18
|
+
- HTTP webhook and admin endpoints
|
|
19
|
+
- Postgres and in-memory storage backends
|
|
20
|
+
- RBAC, PII masking, audit logs, and rate limiting
|
|
21
|
+
- CLI packaging for `npx apphud-mcp`
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Standard
|
|
4
|
+
|
|
5
|
+
We expect respectful, constructive, and professional collaboration.
|
|
6
|
+
|
|
7
|
+
Examples of positive behavior:
|
|
8
|
+
|
|
9
|
+
- giving actionable feedback
|
|
10
|
+
- assuming good intent
|
|
11
|
+
- discussing ideas without personal attacks
|
|
12
|
+
|
|
13
|
+
Examples of unacceptable behavior:
|
|
14
|
+
|
|
15
|
+
- harassment, insults, or discrimination
|
|
16
|
+
- doxxing or sharing private information
|
|
17
|
+
- repeated disruptive behavior in issues/PRs
|
|
18
|
+
|
|
19
|
+
## Enforcement
|
|
20
|
+
|
|
21
|
+
Project maintainers may remove comments, reject contributions, or restrict access for violations.
|
|
22
|
+
|
|
23
|
+
## Reporting
|
|
24
|
+
|
|
25
|
+
Report conduct issues through project maintainers with clear context and evidence.
|
|
26
|
+
|
|
27
|
+
All reports are reviewed and handled with discretion.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Contributing to apphud-mcp
|
|
2
|
+
|
|
3
|
+
Thanks for contributing.
|
|
4
|
+
|
|
5
|
+
## Ground Rules
|
|
6
|
+
|
|
7
|
+
- Keep changes focused and reviewable.
|
|
8
|
+
- Preserve tenant isolation and RBAC behavior.
|
|
9
|
+
- Do not expose secrets or raw PII by default.
|
|
10
|
+
- For behavior changes in tools, update docs and tests in the same PR.
|
|
11
|
+
|
|
12
|
+
## Local Setup
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
npm run typecheck
|
|
17
|
+
npm test
|
|
18
|
+
npm run build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Branch and PR Flow
|
|
22
|
+
|
|
23
|
+
1. Create a branch from `main`.
|
|
24
|
+
2. Implement one logical change per PR.
|
|
25
|
+
3. Include in PR description:
|
|
26
|
+
- problem statement
|
|
27
|
+
- implementation summary
|
|
28
|
+
- validation steps and results
|
|
29
|
+
- backward-compatibility notes
|
|
30
|
+
|
|
31
|
+
## Required Checks Before PR
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm run typecheck
|
|
35
|
+
npm test
|
|
36
|
+
npm run build
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
If you changed packaging or CLI behavior, also run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm pack --dry-run
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Docs Requirements
|
|
46
|
+
|
|
47
|
+
Update docs for any user-visible change:
|
|
48
|
+
|
|
49
|
+
- `README.md` for setup or behavior changes
|
|
50
|
+
- `docs/tools.md` for tool contract changes
|
|
51
|
+
- `docs/implementation-instructions.md` for integration flow changes
|
|
52
|
+
|
|
53
|
+
## Security Expectations
|
|
54
|
+
|
|
55
|
+
- Never commit real API keys, webhook tokens, or DB credentials.
|
|
56
|
+
- Keep secret handling reference-based (`secrets_ref`, `webhook_secret_ref`).
|
|
57
|
+
- If you find a vulnerability, follow `SECURITY.md`.
|
|
58
|
+
|
|
59
|
+
## Commit Message Style
|
|
60
|
+
|
|
61
|
+
Use concise imperative messages, for example:
|
|
62
|
+
|
|
63
|
+
- `Add postgres bootstrap validation`
|
|
64
|
+
- `Document Cursor integration flow`
|
|
65
|
+
|
|
66
|
+
## Questions
|
|
67
|
+
|
|
68
|
+
Open a GitHub issue with:
|
|
69
|
+
|
|
70
|
+
- expected behavior
|
|
71
|
+
- actual behavior
|
|
72
|
+
- reproduction steps
|
|
73
|
+
- logs or payloads with sensitive data redacted
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aso.network
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# apphud-mcp <img src="./assets/apphud-mcp-logo.svg" alt="apphud + mcp" height="28" />
|
|
2
|
+
|
|
3
|
+
Zero-config MCP server for Apphud Dashboard Analytics.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
1. Optional config file (`apphud-mcp.config.json`):
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
2. Cursor MCP config:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"apphud-mcp": {
|
|
19
|
+
"command": "node",
|
|
20
|
+
"args": [
|
|
21
|
+
"/Users/you/apphud-mcp/dist/src/cli.js",
|
|
22
|
+
"start",
|
|
23
|
+
"--config",
|
|
24
|
+
"/Users/you/apphud-mcp/apphud-mcp.config.json"
|
|
25
|
+
],
|
|
26
|
+
"env": {
|
|
27
|
+
"login": "your@apphud.email",
|
|
28
|
+
"password": "your_apphud_password"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
3. Restart MCP server in Cursor.
|
|
36
|
+
|
|
37
|
+
## What Changed
|
|
38
|
+
|
|
39
|
+
- Default mode is analytics-only.
|
|
40
|
+
- No Apphud app API keys required for basic usage.
|
|
41
|
+
- No Postgres required.
|
|
42
|
+
- No webhook/event-store setup required.
|
|
43
|
+
- Apps are fetched directly from Apphud Dashboard API.
|
|
44
|
+
|
|
45
|
+
## Basic Checks
|
|
46
|
+
|
|
47
|
+
Ask MCP to call:
|
|
48
|
+
|
|
49
|
+
1. `apphud.apps.list`
|
|
50
|
+
2. `apphud.analytics.capabilities.get` (with `app_id` from step 1)
|
|
51
|
+
3. `apphud.analytics.revenue.summary` (for a date range)
|
|
52
|
+
|
|
53
|
+
## Available Tools
|
|
54
|
+
|
|
55
|
+
- `apphud.apps.list`
|
|
56
|
+
- `apphud.analytics.events.list`
|
|
57
|
+
- `apphud.analytics.active_subscriptions`
|
|
58
|
+
- `apphud.analytics.capabilities.get`
|
|
59
|
+
- `apphud.analytics.metrics.list`
|
|
60
|
+
- `apphud.analytics.metric.value`
|
|
61
|
+
- `apphud.analytics.metric.timeseries`
|
|
62
|
+
- `apphud.analytics.metric.breakdown`
|
|
63
|
+
- `apphud.analytics.revenue.summary`
|
|
64
|
+
- `apphud.analytics.subscriptions.summary`
|
|
65
|
+
- `apphud.analytics.conversion.trial_to_paid`
|
|
66
|
+
- `apphud.analytics.cohorts.retention`
|
|
67
|
+
- `apphud.analytics.cohorts.ltv`
|
|
68
|
+
- `apphud.analytics.query.raw`
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm install
|
|
74
|
+
npm run ci
|
|
75
|
+
```
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Security fixes are provided for the latest minor release line.
|
|
6
|
+
|
|
7
|
+
| Version | Supported |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| 0.1.x | Yes |
|
|
10
|
+
| < 0.1.0 | No |
|
|
11
|
+
|
|
12
|
+
## Reporting a Vulnerability
|
|
13
|
+
|
|
14
|
+
Please do not open public issues for security vulnerabilities.
|
|
15
|
+
|
|
16
|
+
Use one of these channels:
|
|
17
|
+
|
|
18
|
+
1. GitHub private vulnerability reporting (preferred, if enabled).
|
|
19
|
+
2. If unavailable, contact maintainers directly and include:
|
|
20
|
+
- affected version
|
|
21
|
+
- reproduction steps
|
|
22
|
+
- impact assessment
|
|
23
|
+
- suggested fix (optional)
|
|
24
|
+
|
|
25
|
+
We aim to acknowledge reports within 3 business days.
|
package/SUPPORT.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Support
|
|
2
|
+
|
|
3
|
+
## Documentation
|
|
4
|
+
|
|
5
|
+
Start with:
|
|
6
|
+
|
|
7
|
+
- `README.md`
|
|
8
|
+
- `docs/implementation-instructions.md`
|
|
9
|
+
- `docs/tools.md`
|
|
10
|
+
- `docs/webhook-setup.md`
|
|
11
|
+
- `docs/runbook.md`
|
|
12
|
+
|
|
13
|
+
## Asking for Help
|
|
14
|
+
|
|
15
|
+
Open a GitHub issue and include:
|
|
16
|
+
|
|
17
|
+
- MCP client (Cursor, Claude Desktop, etc.)
|
|
18
|
+
- Node version
|
|
19
|
+
- config snippet (with secrets removed)
|
|
20
|
+
- full error output
|
|
21
|
+
- steps to reproduce
|
|
22
|
+
|
|
23
|
+
## Security Issues
|
|
24
|
+
|
|
25
|
+
Do not post vulnerabilities publicly.
|
|
26
|
+
Follow `SECURITY.md`.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="32" viewBox="0 0 300 32" role="img" aria-label="apphud + mcp">
|
|
2
|
+
<g fill="#0b65d8">
|
|
3
|
+
<path d="M51.3813 9.68308H54.0644V22.9754H51.2337L50.9383 21.7938C49.7239 22.8441 48.288 23.3692 46.6306 23.3692C44.6285 23.3692 42.9629 22.7046 41.6337 21.3754C40.3208 20.0297 39.6644 18.3477 39.6644 16.3292C39.6644 14.3272 40.3208 12.6615 41.6337 11.3323C42.9629 10.0031 44.6285 9.33846 46.6306 9.33846C48.3208 9.33846 49.7731 9.88 50.9875 10.9631L51.3813 9.68308ZM44.3167 18.9385C45.006 19.6277 45.8675 19.9723 46.9013 19.9723C47.9516 19.9723 48.8213 19.6277 49.5106 18.9385C50.1998 18.2492 50.5444 17.3795 50.5444 16.3292C50.5444 15.279 50.1998 14.4092 49.5106 13.72C48.8213 13.0308 47.9598 12.6862 46.926 12.6862C46.2367 12.6862 45.6131 12.8503 45.0552 13.1785C44.5137 13.4903 44.087 13.9251 43.7752 14.4831C43.4634 15.0246 43.3075 15.64 43.3075 16.3292C43.3075 17.3795 43.6439 18.2492 44.3167 18.9385ZM64.7827 9.33846C66.7847 9.33846 68.4422 10.0031 69.755 11.3323C71.0842 12.6615 71.7488 14.3354 71.7488 16.3538C71.7488 18.3723 71.0842 20.0544 69.755 21.4C68.4258 22.7292 66.7683 23.3938 64.7827 23.3938C63.4206 23.3938 62.1981 23.0246 61.115 22.2862V27.8H57.5212V9.70769H59.9088L60.5242 10.9138C61.7058 9.86359 63.1253 9.33846 64.7827 9.33846ZM61.8781 18.9631C62.5673 19.6523 63.4288 19.9969 64.4627 19.9969C65.5129 19.9969 66.3827 19.6523 67.0719 18.9631C67.7612 18.2738 68.1058 17.4041 68.1058 16.3538C68.1058 15.6646 67.9499 15.041 67.6381 14.4831C67.3263 13.9251 66.8914 13.4903 66.3335 13.1785C65.7919 12.8667 65.1765 12.7108 64.4873 12.7108C63.7981 12.7108 63.1745 12.8667 62.6165 13.1785C62.0586 13.4903 61.6237 13.9251 61.3119 14.4831C61.0165 15.041 60.8688 15.6646 60.8688 16.3538C60.8688 17.4041 61.2053 18.2738 61.8781 18.9631ZM81.5615 9.33846C83.5636 9.33846 85.221 10.0031 86.5338 11.3323C87.8631 12.6615 88.5277 14.3354 88.5277 16.3538C88.5277 18.3723 87.8631 20.0544 86.5338 21.4C85.2046 22.7292 83.5472 23.3938 81.5615 23.3938C80.1995 23.3938 78.9769 23.0246 77.8938 22.2862V27.8H74.3V9.70769H76.6877L77.3031 10.9138C78.4846 9.86359 79.9041 9.33846 81.5615 9.33846ZM78.6569 18.9631C79.3462 19.6523 80.2077 19.9969 81.2415 19.9969C82.2918 19.9969 83.1615 19.6523 83.8508 18.9631C84.54 18.2738 84.8846 17.4041 84.8846 16.3538C84.8846 15.6646 84.7287 15.041 84.4169 14.4831C84.1051 13.9251 83.6703 13.4903 83.1123 13.1785C82.5708 12.8667 81.9554 12.7108 81.2662 12.7108C80.5769 12.7108 79.9533 12.8667 79.3954 13.1785C78.8374 13.4903 78.4026 13.9251 78.0908 14.4831C77.7954 15.041 77.6477 15.6646 77.6477 16.3538C77.6477 17.4041 77.9841 18.2738 78.6569 18.9631ZM98.685 9.38769C100.195 9.38769 101.384 9.8882 102.254 10.8892C103.124 11.8903 103.559 13.2359 103.559 14.9262V23H99.965V15.5169C99.965 14.5159 99.7763 13.7692 99.3988 13.2769C99.0378 12.7846 98.4553 12.5385 97.6512 12.5385C96.7486 12.5385 96.0265 12.8503 95.485 13.4738C94.9435 14.0974 94.6727 14.9426 94.6727 16.0092V23H91.0788V4.90769H94.6727V10.9385C95.7558 9.90461 97.0932 9.38769 98.685 9.38769ZM117.433 21.7446C116.3 22.8277 114.791 23.3692 112.903 23.3692C111.033 23.3692 109.523 22.8195 108.374 21.72C107.242 20.6205 106.676 19.1764 106.676 17.3877V9.70769H110.27V16.9692C110.27 17.921 110.499 18.6677 110.959 19.2092C111.435 19.7344 112.083 19.9969 112.903 19.9969C113.74 19.9969 114.389 19.7344 114.848 19.2092C115.324 18.6677 115.562 17.921 115.562 16.9692V9.70769H119.156V17.3877C119.156 19.1928 118.581 20.6451 117.433 21.7446ZM132.197 4.90769H135.791V23H133.133L132.714 21.8923C131.533 22.8933 130.146 23.3938 128.554 23.3938C127.225 23.3938 126.027 23.0903 124.96 22.4831C123.894 21.8759 123.057 21.039 122.45 19.9723C121.859 18.8892 121.563 17.6831 121.563 16.3538C121.563 15.0246 121.859 13.8267 122.45 12.76C123.057 11.6933 123.894 10.8564 124.96 10.2492C126.027 9.64205 127.225 9.33846 128.554 9.33846C129.9 9.33846 131.114 9.69949 132.197 10.4215V4.90769ZM126.24 18.9631C126.93 19.6523 127.791 19.9969 128.825 19.9969C129.875 19.9969 130.745 19.6523 131.434 18.9631C132.123 18.2574 132.468 17.3877 132.468 16.3538C132.468 15.3036 132.123 14.4338 131.434 13.7446C130.745 13.0554 129.875 12.7108 128.825 12.7108C127.791 12.7108 126.93 13.0554 126.24 13.7446C125.551 14.4338 125.207 15.3036 125.207 16.3538C125.207 17.4041 125.551 18.2738 126.24 18.9631Z"/>
|
|
4
|
+
<text x="148" y="22" font-family="Segoe UI, Helvetica, Arial, sans-serif" font-size="14" font-weight="600">+ mcp</text>
|
|
5
|
+
</g>
|
|
6
|
+
</svg>
|
package/dist/src/app.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { loadEnvConfig } from "./config/env.js";
|
|
2
|
+
import { EnvSecretStore } from "./security/secretStore.js";
|
|
3
|
+
import { RateLimiter } from "./security/rateLimiter.js";
|
|
4
|
+
import { AppService } from "./services/appService.js";
|
|
5
|
+
import { AnalyticsService } from "./services/analyticsService.js";
|
|
6
|
+
import { ApphudClient } from "./services/apphudClient.js";
|
|
7
|
+
import { AuditService } from "./services/auditService.js";
|
|
8
|
+
import { ToolGuard } from "./services/toolGuard.js";
|
|
9
|
+
export function createServiceContainer(options) {
|
|
10
|
+
const config = options?.config ?? loadEnvConfig();
|
|
11
|
+
const secretStore = options?.secretStore ?? new EnvSecretStore();
|
|
12
|
+
const appService = new AppService();
|
|
13
|
+
const apphudClient = new ApphudClient(config, secretStore);
|
|
14
|
+
const analyticsService = new AnalyticsService(appService, apphudClient);
|
|
15
|
+
const auditService = new AuditService(config);
|
|
16
|
+
const rateLimiter = new RateLimiter(config.rateLimitPerMinute);
|
|
17
|
+
const toolGuard = new ToolGuard(rateLimiter, auditService);
|
|
18
|
+
return {
|
|
19
|
+
config,
|
|
20
|
+
secretStore,
|
|
21
|
+
appService,
|
|
22
|
+
apphudClient,
|
|
23
|
+
analyticsService,
|
|
24
|
+
auditService,
|
|
25
|
+
toolGuard,
|
|
26
|
+
close: async () => Promise.resolve(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function initializeServiceContainer(_container) {
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { access, writeFile } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { buildExampleSimpleConfig } from "./config/env.js";
|
|
7
|
+
import { startFromConfig } from "./index.js";
|
|
8
|
+
export function parseArgs(argv) {
|
|
9
|
+
const [rawCommand, ...rest] = argv;
|
|
10
|
+
const helpRequested = rawCommand === "--help" || rawCommand === "-h" || argv.includes("--help") || argv.includes("-h");
|
|
11
|
+
const command = helpRequested ? "help" : rawCommand === "init-config" || rawCommand === "help" ? rawCommand : "start";
|
|
12
|
+
const args = rawCommand === command ? rest : argv;
|
|
13
|
+
const readArgValue = (flag) => {
|
|
14
|
+
const index = args.indexOf(flag);
|
|
15
|
+
if (index < 0) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return args[index + 1];
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
command,
|
|
22
|
+
options: {
|
|
23
|
+
configPath: readArgValue("--config"),
|
|
24
|
+
configJson: readArgValue("--config-json"),
|
|
25
|
+
out: readArgValue("--out"),
|
|
26
|
+
force: args.includes("--force"),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function printHelp() {
|
|
31
|
+
console.error(`apphud-mcp
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
apphud-mcp start [--config <path>] [--config-json '<json>']
|
|
35
|
+
apphud-mcp init-config [--out <path>] [--force]
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
async function ensureWritable(filePath, force = false) {
|
|
39
|
+
if (force) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await access(filePath, constants.F_OK);
|
|
44
|
+
throw new Error(`File already exists: ${filePath}. Use --force to overwrite.`);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error instanceof Error && error.message.startsWith("File already exists")) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function writeConfigTemplate(outPath, force = false) {
|
|
53
|
+
const absPath = path.resolve(outPath);
|
|
54
|
+
await ensureWritable(absPath, force);
|
|
55
|
+
const template = buildExampleSimpleConfig();
|
|
56
|
+
await writeFile(absPath, `${JSON.stringify(template, null, 2)}\n`, "utf8");
|
|
57
|
+
console.error(`Config template created: ${absPath}`);
|
|
58
|
+
}
|
|
59
|
+
async function main() {
|
|
60
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
61
|
+
switch (parsed.command) {
|
|
62
|
+
case "help":
|
|
63
|
+
printHelp();
|
|
64
|
+
return;
|
|
65
|
+
case "init-config": {
|
|
66
|
+
const outPath = parsed.options.out ?? "./apphud-mcp.config.json";
|
|
67
|
+
await writeConfigTemplate(outPath, parsed.options.force ?? false);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
case "start":
|
|
71
|
+
default: {
|
|
72
|
+
const runtime = await startFromConfig(parsed.options);
|
|
73
|
+
process.on("SIGINT", () => {
|
|
74
|
+
runtime
|
|
75
|
+
.shutdown()
|
|
76
|
+
.then(() => process.exit(0))
|
|
77
|
+
.catch(() => process.exit(1));
|
|
78
|
+
});
|
|
79
|
+
process.on("SIGTERM", () => {
|
|
80
|
+
runtime
|
|
81
|
+
.shutdown()
|
|
82
|
+
.then(() => process.exit(0))
|
|
83
|
+
.catch(() => process.exit(1));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const isDirectRun = process.argv[1] ? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false;
|
|
89
|
+
if (isDirectRun) {
|
|
90
|
+
main().catch((error) => {
|
|
91
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
const VALID_ROLES = new Set(["analyst", "support", "admin"]);
|
|
5
|
+
const VALID_STORAGE_BACKENDS = new Set(["postgres", "memory"]);
|
|
6
|
+
const VALID_APP_STATUSES = new Set(["active", "disabled"]);
|
|
7
|
+
function parseNumber(value, fallback) {
|
|
8
|
+
if (value === undefined || value === null || value === "") {
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
const parsed = Number(value);
|
|
12
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
13
|
+
}
|
|
14
|
+
function parseBoolean(value, fallback) {
|
|
15
|
+
if (value === undefined || value === null || value === "") {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "boolean") {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
22
|
+
}
|
|
23
|
+
function parseRole(value) {
|
|
24
|
+
if (typeof value !== "string") {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
if (!VALID_ROLES.has(value)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function readConfigFile(filePath) {
|
|
33
|
+
const absPath = path.resolve(filePath);
|
|
34
|
+
try {
|
|
35
|
+
const raw = readFileSync(absPath, "utf8");
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code === "ENOENT") {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function parseConfigJson(json) {
|
|
46
|
+
return JSON.parse(json);
|
|
47
|
+
}
|
|
48
|
+
function parseStorageBackend(value) {
|
|
49
|
+
if (typeof value !== "string") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (!VALID_STORAGE_BACKENDS.has(value)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function toSafeAppId(rawValue, fallbackIndex) {
|
|
58
|
+
const normalized = rawValue
|
|
59
|
+
.trim()
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
62
|
+
.replace(/^_+|_+$/g, "")
|
|
63
|
+
.slice(0, 64);
|
|
64
|
+
if (normalized.length > 0) {
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
return `app_${fallbackIndex + 1}`;
|
|
68
|
+
}
|
|
69
|
+
function parseBootstrapApps(value, options) {
|
|
70
|
+
if (!Array.isArray(value)) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const apps = [];
|
|
74
|
+
const usedAppIds = new Set();
|
|
75
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
76
|
+
const raw = value[index];
|
|
77
|
+
if (!raw || typeof raw !== "object") {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const candidate = raw;
|
|
81
|
+
if (typeof candidate.secrets_ref !== "string") {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const displayName = (typeof candidate.name === "string" && candidate.name.trim().length > 0
|
|
85
|
+
? candidate.name
|
|
86
|
+
: undefined) ??
|
|
87
|
+
(typeof candidate.app_name === "string" && candidate.app_name.trim().length > 0
|
|
88
|
+
? candidate.app_name
|
|
89
|
+
: undefined) ??
|
|
90
|
+
(typeof candidate.app_id === "string" && candidate.app_id.trim().length > 0 ? candidate.app_id : undefined);
|
|
91
|
+
if (!displayName) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const tenantId = (typeof candidate.tenant_id === "string" && candidate.tenant_id.trim().length > 0
|
|
95
|
+
? candidate.tenant_id
|
|
96
|
+
: undefined) ??
|
|
97
|
+
options.defaultTenantId ??
|
|
98
|
+
"tenant_default";
|
|
99
|
+
let appId = (typeof candidate.app_id === "string" && candidate.app_id.trim().length > 0
|
|
100
|
+
? candidate.app_id.trim()
|
|
101
|
+
: undefined) ?? toSafeAppId(displayName, index);
|
|
102
|
+
while (usedAppIds.has(appId)) {
|
|
103
|
+
appId = `${appId}_${index + 1}`;
|
|
104
|
+
}
|
|
105
|
+
usedAppIds.add(appId);
|
|
106
|
+
const status = VALID_APP_STATUSES.has(candidate.status)
|
|
107
|
+
? candidate.status
|
|
108
|
+
: "active";
|
|
109
|
+
apps.push({
|
|
110
|
+
appId,
|
|
111
|
+
tenantId,
|
|
112
|
+
name: displayName,
|
|
113
|
+
status,
|
|
114
|
+
secretsRef: candidate.secrets_ref,
|
|
115
|
+
webhookSecretRef: typeof candidate.webhook_secret_ref === "string" ? candidate.webhook_secret_ref : undefined,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return apps;
|
|
119
|
+
}
|
|
120
|
+
function parseEnvAppsJson(value, options) {
|
|
121
|
+
if (!value) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return parseBootstrapApps(JSON.parse(value), options);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function maybeReadSimpleConfig(options) {
|
|
132
|
+
if (options.configJson) {
|
|
133
|
+
return parseConfigJson(options.configJson);
|
|
134
|
+
}
|
|
135
|
+
if (options.configPath) {
|
|
136
|
+
return readConfigFile(options.configPath);
|
|
137
|
+
}
|
|
138
|
+
if (process.env.APPHUD_MCP_CONFIG) {
|
|
139
|
+
return parseConfigJson(process.env.APPHUD_MCP_CONFIG);
|
|
140
|
+
}
|
|
141
|
+
if (process.env.APPHUD_MCP_CONFIG_PATH) {
|
|
142
|
+
return readConfigFile(process.env.APPHUD_MCP_CONFIG_PATH);
|
|
143
|
+
}
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
export function loadEnvConfig(options = {}) {
|
|
147
|
+
const simpleConfig = maybeReadSimpleConfig(options);
|
|
148
|
+
const databaseUrlRaw = simpleConfig.database_url ?? process.env.DATABASE_URL;
|
|
149
|
+
const databaseUrl = typeof databaseUrlRaw === "string" && databaseUrlRaw.trim().length > 0 ? databaseUrlRaw : undefined;
|
|
150
|
+
const parsedStorageBackend = parseStorageBackend(simpleConfig.storage?.backend) ?? parseStorageBackend(process.env.STORAGE_BACKEND);
|
|
151
|
+
const storageBackend = parsedStorageBackend ?? (databaseUrl ? "postgres" : "memory");
|
|
152
|
+
if (storageBackend === "postgres" && !databaseUrl) {
|
|
153
|
+
throw new Error("DATABASE_URL is required for postgres storage backend. Set database_url or switch to storage.backend=memory");
|
|
154
|
+
}
|
|
155
|
+
const defaultTenantIdRaw = simpleConfig.defaults?.tenant_id ?? process.env.DEFAULT_TENANT_ID;
|
|
156
|
+
const defaultTenantId = typeof defaultTenantIdRaw === "string" && defaultTenantIdRaw.trim().length > 0
|
|
157
|
+
? defaultTenantIdRaw
|
|
158
|
+
: "tenant_default";
|
|
159
|
+
const defaultRole = parseRole(simpleConfig.defaults?.role) ?? parseRole(process.env.DEFAULT_ROLE) ?? "admin";
|
|
160
|
+
const configApps = parseBootstrapApps(simpleConfig.apps, { defaultTenantId });
|
|
161
|
+
const envApps = parseEnvAppsJson(process.env.APPHUD_MCP_APPS_JSON, { defaultTenantId });
|
|
162
|
+
const bootstrapApps = configApps.length > 0 ? configApps : envApps;
|
|
163
|
+
const analyticsApiBaseUrlRaw = simpleConfig.apphud?.analytics_api_base_url ?? process.env.APPHUD_ANALYTICS_API_BASE_URL;
|
|
164
|
+
const analyticsApiBaseUrl = typeof analyticsApiBaseUrlRaw === "string" && analyticsApiBaseUrlRaw.trim().length > 0
|
|
165
|
+
? analyticsApiBaseUrlRaw
|
|
166
|
+
: "https://api-legacy.apphud.com";
|
|
167
|
+
const analyticsAuthHeaderRaw = simpleConfig.apphud?.analytics_auth_header ?? process.env.APPHUD_ANALYTICS_AUTH_HEADER;
|
|
168
|
+
const analyticsAuthHeader = typeof analyticsAuthHeaderRaw === "string" && analyticsAuthHeaderRaw.trim().length > 0
|
|
169
|
+
? analyticsAuthHeaderRaw
|
|
170
|
+
: "Cookie";
|
|
171
|
+
const analyticsAuthPrefixRaw = simpleConfig.apphud?.analytics_auth_prefix ?? process.env.APPHUD_ANALYTICS_AUTH_PREFIX;
|
|
172
|
+
const analyticsAuthPrefix = typeof analyticsAuthPrefixRaw === "string" ? analyticsAuthPrefixRaw : "";
|
|
173
|
+
const analyticsLoginEmailSecretRefRaw = simpleConfig.apphud?.analytics_login_email_secret_ref ?? process.env.APPHUD_ANALYTICS_LOGIN_EMAIL_SECRET_REF;
|
|
174
|
+
const analyticsLoginEmailSecretRef = typeof analyticsLoginEmailSecretRefRaw === "string" && analyticsLoginEmailSecretRefRaw.trim().length > 0
|
|
175
|
+
? analyticsLoginEmailSecretRefRaw
|
|
176
|
+
: "login";
|
|
177
|
+
const analyticsLoginPasswordSecretRefRaw = simpleConfig.apphud?.analytics_login_password_secret_ref ?? process.env.APPHUD_ANALYTICS_LOGIN_PASSWORD_SECRET_REF;
|
|
178
|
+
const analyticsLoginPasswordSecretRef = typeof analyticsLoginPasswordSecretRefRaw === "string" && analyticsLoginPasswordSecretRefRaw.trim().length > 0
|
|
179
|
+
? analyticsLoginPasswordSecretRefRaw
|
|
180
|
+
: "password";
|
|
181
|
+
return {
|
|
182
|
+
nodeEnv: simpleConfig.node_env ?? process.env.NODE_ENV ?? "development",
|
|
183
|
+
port: parseNumber(simpleConfig.transport?.port ?? process.env.PORT, 8080),
|
|
184
|
+
databaseUrl,
|
|
185
|
+
storageBackend,
|
|
186
|
+
bootstrapApps,
|
|
187
|
+
defaultTenantId,
|
|
188
|
+
defaultRole,
|
|
189
|
+
defaultUserId: typeof simpleConfig.defaults?.user_id === "string" && simpleConfig.defaults.user_id.trim().length > 0
|
|
190
|
+
? simpleConfig.defaults.user_id
|
|
191
|
+
: typeof process.env.DEFAULT_USER_ID === "string" && process.env.DEFAULT_USER_ID.trim().length > 0
|
|
192
|
+
? process.env.DEFAULT_USER_ID
|
|
193
|
+
: "local_user",
|
|
194
|
+
customerSnapshotTtlMinutes: parseNumber(simpleConfig.limits?.customer_snapshot_ttl_minutes ?? process.env.CUSTOMER_SNAPSHOT_TTL_MINUTES, 10),
|
|
195
|
+
rateLimitPerMinute: parseNumber(simpleConfig.limits?.rate_limit_per_minute ?? process.env.RATE_LIMIT_PER_MINUTE, 120),
|
|
196
|
+
auditHashSalt: simpleConfig.security?.audit_hash_salt ?? process.env.AUDIT_HASH_SALT ?? "dev-salt",
|
|
197
|
+
apphudCustomersApiBaseUrl: simpleConfig.apphud?.customers_api_base_url ??
|
|
198
|
+
process.env.APPHUD_CUSTOMERS_API_BASE_URL ??
|
|
199
|
+
"https://api.apphud.com/v1/customers",
|
|
200
|
+
apphudCustomersApiAuthHeader: simpleConfig.apphud?.auth_header ?? process.env.APPHUD_CUSTOMERS_API_AUTH_HEADER ?? "Authorization",
|
|
201
|
+
apphudCustomersApiAuthPrefix: simpleConfig.apphud?.auth_prefix ?? process.env.APPHUD_CUSTOMERS_API_AUTH_PREFIX ?? "Bearer ",
|
|
202
|
+
apphudAnalyticsApiBaseUrl: analyticsApiBaseUrl,
|
|
203
|
+
apphudAnalyticsAuthHeader: analyticsAuthHeader,
|
|
204
|
+
apphudAnalyticsAuthPrefix: analyticsAuthPrefix,
|
|
205
|
+
apphudAnalyticsAuthSecretRef: simpleConfig.apphud?.analytics_auth_secret_ref ??
|
|
206
|
+
process.env.APPHUD_ANALYTICS_AUTH_SECRET_REF,
|
|
207
|
+
apphudAnalyticsLoginPath: simpleConfig.apphud?.analytics_login_path ??
|
|
208
|
+
process.env.APPHUD_ANALYTICS_LOGIN_PATH ??
|
|
209
|
+
"/sessions",
|
|
210
|
+
apphudAnalyticsLoginEmailSecretRef: analyticsLoginEmailSecretRef,
|
|
211
|
+
apphudAnalyticsLoginPasswordSecretRef: analyticsLoginPasswordSecretRef,
|
|
212
|
+
apphudWebhookTokenHeader: simpleConfig.security?.webhook_token_header ?? process.env.APPHUD_WEBHOOK_TOKEN_HEADER ?? "x-apphud-token",
|
|
213
|
+
httpEnabled: parseBoolean(simpleConfig.transport?.http_enabled ?? process.env.HTTP_ENABLED, false),
|
|
214
|
+
mcpStdioEnabled: parseBoolean(simpleConfig.transport?.mcp_stdio_enabled ?? process.env.MCP_STDIO_ENABLED, true),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function buildExampleSimpleConfig() {
|
|
218
|
+
return {
|
|
219
|
+
storage: {
|
|
220
|
+
backend: "memory",
|
|
221
|
+
},
|
|
222
|
+
defaults: {
|
|
223
|
+
tenant_id: "tenant_default",
|
|
224
|
+
role: "admin",
|
|
225
|
+
user_id: "local_user",
|
|
226
|
+
},
|
|
227
|
+
transport: {
|
|
228
|
+
port: 8080,
|
|
229
|
+
http_enabled: false,
|
|
230
|
+
mcp_stdio_enabled: true,
|
|
231
|
+
},
|
|
232
|
+
limits: {
|
|
233
|
+
customer_snapshot_ttl_minutes: 10,
|
|
234
|
+
rate_limit_per_minute: 120,
|
|
235
|
+
},
|
|
236
|
+
security: {
|
|
237
|
+
audit_hash_salt: "replace-me",
|
|
238
|
+
webhook_token_header: "x-apphud-token",
|
|
239
|
+
},
|
|
240
|
+
apphud: {
|
|
241
|
+
analytics_api_base_url: "https://api-legacy.apphud.com",
|
|
242
|
+
analytics_auth_header: "Cookie",
|
|
243
|
+
analytics_auth_prefix: "",
|
|
244
|
+
analytics_login_path: "/sessions",
|
|
245
|
+
analytics_login_email_secret_ref: "login",
|
|
246
|
+
analytics_login_password_secret_ref: "password",
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|