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.
package/README.md CHANGED
@@ -1,20 +1,17 @@
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 with local SQLite storage.
3
+ MCP server for Apphud dashboard analytics endpoints (apps, events, metrics, cohorts).
4
4
 
5
5
  ## Quick Start
6
6
 
7
- 1. Cursor MCP config (no extra config file required):
7
+ 1. Add MCP server:
8
8
 
9
9
  ```json
10
10
  {
11
11
  "mcpServers": {
12
12
  "apphud-mcp": {
13
- "command": "node",
14
- "args": [
15
- "/Users/you/apphud-mcp/dist/src/cli.js",
16
- "start"
17
- ],
13
+ "command": "npx",
14
+ "args": ["-y", "apphud-mcp@0.2.2", "start"],
18
15
  "env": {
19
16
  "login": "your@apphud.email",
20
17
  "password": "your_apphud_password"
@@ -24,56 +21,87 @@ Zero-config MCP server for Apphud Dashboard Analytics with local SQLite storage.
24
21
  }
25
22
  ```
26
23
 
27
- 2. Restart MCP server in Cursor.
24
+ 2. Restart MCP server.
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`)
28
29
 
29
- 3. Check local ETL status:
30
- - `apphud_etl_local_status`
31
- - `apphud_apps_list_local`
32
- - `apphud_dashboard_local`
30
+ ## HTTP Tool Calls
33
31
 
34
- Optional config file (`apphud-mcp.config.json`) is only needed if you want custom paths or custom ETL polling.
32
+ If `HTTP_ENABLED=true`, you can call tools over HTTP (stable payload path for web clients):
35
33
 
36
- ## What Changed
37
-
38
- - Default mode is analytics-only.
39
- - No Postgres required (SQLite is default).
40
- - No webhook/event-store setup required.
41
- - Apps are fetched directly from Apphud Dashboard API.
42
- - ETL worker can run hourly and auto-sync app API keys from Dashboard (`settings/general` fallback).
43
-
44
- ## Basic Checks
45
-
46
- Ask MCP to call:
47
-
48
- 1. `apphud_apps_list`
49
- 2. `apphud_analytics_capabilities_get` (with `app_id` from step 1)
50
- 3. `apphud_analytics_revenue_summary` (for a date range)
51
-
52
- ## Available Tools
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
+ ```
53
47
 
54
- Local-first tools (higher priority):
55
- - `apphud_etl_local_status`
56
- - `apphud_apps_list_local`
57
- - `apphud_dashboard_local`
58
- - `apphud_analytics_events_list_local`
59
- - `apphud_analytics_active_subscriptions_local`
60
- - `apphud_analytics_capabilities_get_local`
61
- - `apphud_analytics_metrics_list_local`
62
- - `apphud_analytics_metric_value_local`
63
- - `apphud_analytics_metric_timeseries_local`
64
- - `apphud_analytics_metric_breakdown_local`
65
- - `apphud_analytics_revenue_summary_local`
66
- - `apphud_analytics_subscriptions_summary_local`
67
- - `apphud_analytics_conversion_trial_to_paid_local`
68
- - `apphud_analytics_cohorts_retention_local`
69
- - `apphud_analytics_cohorts_ltv_local`
70
- - `apphud_analytics_query_raw_local`
48
+ The endpoint also accepts `toolName` and stringified `arguments` JSON for compatibility with buggy wrappers.
49
+
50
+ ## ETL Setup
51
+
52
+ <details>
53
+ <summary>Google Cloud Storage (GCS) setup</summary>
54
+
55
+ 1. In Apphud:
56
+ - Open `Integrations` -> `GCS ETL` -> `Add connection`.
57
+ - Enter project/service account/bucket and enable export for apps.
58
+ 2. In Google Cloud Console:
59
+ - Create/select a bucket in [Cloud Storage](https://console.cloud.google.com/storage/browser).
60
+ - Create a Service Account in [IAM](https://console.cloud.google.com/iam-admin/serviceaccounts).
61
+ - Create JSON key for that service account.
62
+ - Grant access to the bucket (read for MCP side, write for Apphud side as needed by your policy).
63
+ 3. MCP env vars:
64
+ - `ETL_SOURCE=gcs`
65
+ - `ETL_GCS_BUCKET=<bucket-name>`
66
+ - `ETL_GCS_PREFIX=<optional-prefix>`
67
+ - `GOOGLE_APPLICATION_CREDENTIALS=<path-to-service-account-json>`
68
+ 4. Ensure `gsutil` is installed and authenticated in runtime environment.
69
+
70
+ </details>
71
+
72
+ <details>
73
+ <summary>Amazon S3 setup</summary>
74
+
75
+ 1. In Apphud:
76
+ - Open `Integrations` -> `S3 ETL` -> `Add connection`.
77
+ - Fill bucket/credentials/region and enable export for apps.
78
+ 2. In AWS Console:
79
+ - Create/select bucket in [S3](https://s3.console.aws.amazon.com/s3/home).
80
+ - Create IAM user/role with bucket access.
81
+ - Generate access key (if using user credentials).
82
+ 3. MCP env vars:
83
+ - `ETL_SOURCE=s3`
84
+ - `ETL_S3_BUCKET=<bucket-name>`
85
+ - `ETL_S3_PREFIX=<optional-prefix>`
86
+ - `AWS_ACCESS_KEY_ID=<key-id>`
87
+ - `AWS_SECRET_ACCESS_KEY=<secret>`
88
+ - `AWS_REGION=<region>`
89
+ 4. Ensure AWS CLI (`aws`) is installed and usable in runtime environment.
90
+
91
+ </details>
92
+
93
+ ## Defaults
94
+
95
+ - SQLite DB: `.apphud-mcp/apphud.db`
96
+ - Incoming ETL files: `.apphud-etl/incoming`
97
+ - Poll interval: 60 minutes
98
+ - ETL enabled: `true`
99
+ - ETL source default: `none` (set `ETL_SOURCE=gcs` or `ETL_SOURCE=s3`)
100
+
101
+ ## Useful Tools
71
102
 
72
- Remote tools (still available):
73
103
  - `apphud_apps_list`
74
104
  - `apphud_analytics_events_list`
75
- - `apphud_analytics_active_subscriptions`
76
- - `apphud_analytics_capabilities_get`
77
105
  - `apphud_analytics_metrics_list`
78
106
  - `apphud_analytics_metric_value`
79
107
  - `apphud_analytics_metric_timeseries`
package/dist/src/cli.js CHANGED
@@ -34,26 +34,32 @@ Usage:
34
34
  apphud-mcp start [--config <path>] [--config-json '<json>']
35
35
  apphud-mcp init-config [--out <path>] [--force]
36
36
 
37
- Zero-config quick start (ETL enabled by default):
37
+ Zero-config quick start:
38
38
  1) Add MCP server using:
39
- npx -y apphud-mcp@0.2.1 start
39
+ npx -y apphud-mcp@0.2.2 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
50
50
  - ETL storage: .apphud-etl
51
51
  - Poll interval: 60 minutes
52
- - Remote ETL fetch: enabled
52
+ - ETL worker enabled; source is none until you set GCS/S3
53
53
 
54
- Optional disable flags:
55
- ETL_ENABLED=false
56
- ETL_REMOTE_FETCH_ENABLED=false
54
+ Storage source setup:
55
+ GCS:
56
+ ETL_SOURCE=gcs
57
+ ETL_GCS_BUCKET=<bucket>
58
+ ETL_GCS_PREFIX=<prefix optional>
59
+ S3:
60
+ ETL_SOURCE=s3
61
+ ETL_S3_BUCKET=<bucket>
62
+ ETL_S3_PREFIX=<prefix optional>
57
63
  `);
58
64
  }
59
65
  async function ensureWritable(filePath, force = false) {
@@ -190,18 +190,16 @@ export function loadEnvConfig(options = {}) {
190
190
  : `${etlStorageDir}/incoming`;
191
191
  const etlTenantIdRaw = simpleConfig.etl?.tenant_id ?? process.env.ETL_TENANT_ID;
192
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}";
193
+ const etlSourceRaw = simpleConfig.etl?.source ?? process.env.ETL_SOURCE;
194
+ const etlSource = etlSourceRaw === "gcs" || etlSourceRaw === "s3" || etlSourceRaw === "none" ? etlSourceRaw : "none";
195
+ const etlGcsBucketRaw = simpleConfig.etl?.gcs_bucket ?? process.env.ETL_GCS_BUCKET;
196
+ const etlGcsBucket = typeof etlGcsBucketRaw === "string" && etlGcsBucketRaw.trim().length > 0 ? etlGcsBucketRaw.trim() : undefined;
197
+ const etlGcsPrefixRaw = simpleConfig.etl?.gcs_prefix ?? process.env.ETL_GCS_PREFIX;
198
+ const etlGcsPrefix = typeof etlGcsPrefixRaw === "string" && etlGcsPrefixRaw.trim().length > 0 ? etlGcsPrefixRaw.trim() : undefined;
199
+ const etlS3BucketRaw = simpleConfig.etl?.s3_bucket ?? process.env.ETL_S3_BUCKET;
200
+ const etlS3Bucket = typeof etlS3BucketRaw === "string" && etlS3BucketRaw.trim().length > 0 ? etlS3BucketRaw.trim() : undefined;
201
+ const etlS3PrefixRaw = simpleConfig.etl?.s3_prefix ?? process.env.ETL_S3_PREFIX;
202
+ const etlS3Prefix = typeof etlS3PrefixRaw === "string" && etlS3PrefixRaw.trim().length > 0 ? etlS3PrefixRaw.trim() : undefined;
205
203
  return {
206
204
  nodeEnv: simpleConfig.node_env ?? process.env.NODE_ENV ?? "development",
207
205
  port: parseNumber(simpleConfig.transport?.port ?? process.env.PORT, 8080),
@@ -234,9 +232,6 @@ export function loadEnvConfig(options = {}) {
234
232
  "/sessions",
235
233
  apphudAnalyticsLoginEmailSecretRef: analyticsLoginEmailSecretRef,
236
234
  apphudAnalyticsLoginPasswordSecretRef: analyticsLoginPasswordSecretRef,
237
- apphudEtlExportsApiBaseUrl,
238
- apphudEtlExportsListPath,
239
- apphudEtlExportsDownloadPath,
240
235
  apphudWebhookTokenHeader: simpleConfig.security?.webhook_token_header ?? process.env.APPHUD_WEBHOOK_TOKEN_HEADER ?? "x-apphud-token",
241
236
  etlEnabled: parseBoolean(simpleConfig.etl?.enabled ?? process.env.ETL_ENABLED, true),
242
237
  etlTenantId,
@@ -244,7 +239,11 @@ export function loadEnvConfig(options = {}) {
244
239
  etlStorageDir,
245
240
  etlIncomingDir,
246
241
  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, true),
242
+ etlSource,
243
+ etlGcsBucket,
244
+ etlGcsPrefix,
245
+ etlS3Bucket,
246
+ etlS3Prefix,
248
247
  httpEnabled: parseBoolean(simpleConfig.transport?.http_enabled ?? process.env.HTTP_ENABLED, false),
249
248
  mcpStdioEnabled: parseBoolean(simpleConfig.transport?.mcp_stdio_enabled ?? process.env.MCP_STDIO_ENABLED, true),
250
249
  };
@@ -262,7 +261,11 @@ export function buildExampleSimpleConfig() {
262
261
  storage_dir: ".apphud-etl",
263
262
  incoming_dir: ".apphud-etl/incoming",
264
263
  alerts_stale_hours: 30,
265
- remote_fetch_enabled: true,
264
+ source: "none",
265
+ gcs_bucket: "your-gcs-bucket",
266
+ gcs_prefix: "apphud/exports",
267
+ s3_bucket: "your-s3-bucket",
268
+ s3_prefix: "apphud/exports",
266
269
  },
267
270
  defaults: {
268
271
  tenant_id: "tenant_default",
@@ -289,9 +292,6 @@ export function buildExampleSimpleConfig() {
289
292
  analytics_login_path: "/sessions",
290
293
  analytics_login_email_secret_ref: "login",
291
294
  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}",
295
295
  },
296
296
  };
297
297
  }
@@ -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;