@xns-cloud/relayer-mcp 0.4.0 → 0.5.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/README.md +2 -2
- package/package.json +4 -2
- package/src/templates/docker-compose.yml +57 -13
- package/src/tools/checkRelayerHealth.js +31 -4
- package/src/tools/installRelayer.js +41 -18
package/README.md
CHANGED
|
@@ -46,8 +46,8 @@ No separate install step required.
|
|
|
46
46
|
| 1 | `check_prerequisites` | Verify Docker (local or remote), ports (8888, 9000), an existing installation, disk, and network connectivity. |
|
|
47
47
|
| 2 | `register_account` | Register an XNS account (email + password) via Console2. |
|
|
48
48
|
| 3 | `check_email_verified` | Poll email verification status (15s interval, 30-min timeout). |
|
|
49
|
-
| 4 | `install_relayer` |
|
|
50
|
-
| 5 | `check_relayer_health` | Poll UI, S3, and
|
|
49
|
+
| 4 | `install_relayer` | Fetch the canonical beta channel bundle — relayer + Prometheus/Grafana monitoring stack (`https://releases.scpri.me/relayer/beta/docker-compose.yml`, anonymous pull, no `docker login`) — write the `.env`, and start the containers. Falls back to a bundled service-parity copy if the fetch fails. **Fresh installs only** — see [Fresh installs vs. existing deployments](#fresh-installs-vs-existing-deployments). The user authors nothing; `compose_url` is an optional override for custom installs. |
|
|
50
|
+
| 5 | `check_relayer_health` | Poll UI, S3, HostIO, and the monitoring sidecars (10s interval, 300s timeout). A missing monitoring stack reports as degraded without blocking the flow. Targets the Docker host automatically. |
|
|
51
51
|
| 6 | `start_claim` | Initiate a claim session — returns a URL for browser confirmation. |
|
|
52
52
|
| 7 | `check_claim_status` | Poll claim state (STATE_1 / STATE_2 / STATE_3). |
|
|
53
53
|
| 8 | `get_host_tags` | Retrieve available host tags for VPD configuration. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xns-cloud/relayer-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "MCP server for XNS Relayer onboarding — drives a complete Relayer setup conversationally over stdio, npx-ready",
|
|
5
5
|
"author": "SCP Corp",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -47,7 +47,9 @@
|
|
|
47
47
|
"jest": "^30.4.2"
|
|
48
48
|
},
|
|
49
49
|
"jest": {
|
|
50
|
-
"testMatch": [
|
|
50
|
+
"testMatch": [
|
|
51
|
+
"**/src/__tests__/**/*.test.js"
|
|
52
|
+
],
|
|
51
53
|
"testEnvironment": "node",
|
|
52
54
|
"forceExit": true
|
|
53
55
|
}
|
|
@@ -1,29 +1,38 @@
|
|
|
1
|
-
# Bundled by @xns-cloud/relayer-mcp — the released
|
|
2
|
-
# Source of truth: the beta
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
1
|
+
# Bundled by @xns-cloud/relayer-mcp — OFFLINE FALLBACK for the released install.
|
|
2
|
+
# Source of truth: the beta channel bundle on the XNS releases registry —
|
|
3
|
+
# https://releases.scpri.me/relayer/beta/docker-compose.yml (versioned in the
|
|
4
|
+
# deploy repo, shipped by `deploy.py promote`). install_relayer fetches that URL
|
|
5
|
+
# by default and only writes this copy when the fetch fails, so this file MUST
|
|
6
|
+
# keep service parity with the channel bundle: relayer + the monitoring stack
|
|
7
|
+
# (Prometheus + Grafana + node-exporter) that powers the dashboards under
|
|
8
|
+
# Monitoring in the web UI. The jest contract tests enforce the parity.
|
|
9
|
+
#
|
|
10
|
+
# NEVER point an image at Docker Hub scprime/* — those are production fleet
|
|
11
|
+
# tags, not a release channel. The one allowed upstream image is
|
|
12
|
+
# prom/node-exporter (public, multi-arch, version-pinned — matches the channel
|
|
13
|
+
# bundle; nothing to rebuild).
|
|
8
14
|
#
|
|
9
15
|
# Pre-release: pinned to :beta-latest so beta testers exercise the current
|
|
10
|
-
# Relayer agentically. Flip
|
|
16
|
+
# Relayer agentically. Flip these tags to :stable-latest at general release.
|
|
11
17
|
#
|
|
12
|
-
# A first-time user
|
|
18
|
+
# A first-time user never hand-writes this file or the .env — install_relayer
|
|
13
19
|
# writes both. Differences from the channel compose, by design:
|
|
14
|
-
# - data lives in ./data (relative to this file) instead of a named
|
|
15
|
-
# so the same file works on Windows, macOS, and Linux and the data
|
|
16
|
-
# to find next to the compose.
|
|
20
|
+
# - relayer data lives in ./data (relative to this file) instead of a named
|
|
21
|
+
# volume, so the same file works on Windows, macOS, and Linux and the data
|
|
22
|
+
# is easy to find next to the compose.
|
|
17
23
|
# - SMB ports 139/445 are intentionally NOT exposed: 445 is held by Windows
|
|
18
24
|
# SMB on consumer hosts and would fail the install. S3 onboarding uses :9000.
|
|
19
25
|
# - pull_policy: always so `docker compose up -d` fetches the current beta
|
|
20
|
-
#
|
|
26
|
+
# images without a separate `docker compose pull` step.
|
|
21
27
|
services:
|
|
22
28
|
xns:
|
|
29
|
+
# container_name is load-bearing: the baked-in prometheus.yml scrapes the
|
|
30
|
+
# relayer by this name, and the install preflight keys on it.
|
|
23
31
|
container_name: xns-relayer
|
|
24
32
|
image: releases.scpri.me/xns-relayer:beta-latest
|
|
25
33
|
pull_policy: always
|
|
26
34
|
restart: unless-stopped
|
|
35
|
+
privileged: true # samba / fuse / disk management (matches channel compose)
|
|
27
36
|
volumes:
|
|
28
37
|
- ./data:/relayer
|
|
29
38
|
ports:
|
|
@@ -31,3 +40,38 @@ services:
|
|
|
31
40
|
- ${S3_PORT:-9000}:9000
|
|
32
41
|
env_file:
|
|
33
42
|
- .env
|
|
43
|
+
|
|
44
|
+
# Monitoring stack — powers the dashboards under Monitoring in the web UI.
|
|
45
|
+
# Not host-exposed: Grafana is reached only through the relayer's /grafana
|
|
46
|
+
# reverse proxy and the in-network Prometheus scrape.
|
|
47
|
+
prometheus:
|
|
48
|
+
image: releases.scpri.me/relayer-prometheus:beta-latest
|
|
49
|
+
pull_policy: always
|
|
50
|
+
restart: unless-stopped
|
|
51
|
+
volumes:
|
|
52
|
+
- prometheus_data:/prometheus
|
|
53
|
+
|
|
54
|
+
grafana:
|
|
55
|
+
image: releases.scpri.me/relayer-grafana:beta-latest
|
|
56
|
+
pull_policy: always
|
|
57
|
+
restart: unless-stopped
|
|
58
|
+
environment:
|
|
59
|
+
- GF_AUTH_ANONYMOUS_ENABLED=true
|
|
60
|
+
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
|
|
61
|
+
- GF_SERVER_SERVE_FROM_SUB_PATH=true
|
|
62
|
+
- GF_SERVER_ROOT_URL=%(protocol)s://%(domain)s/grafana
|
|
63
|
+
- GF_SECURITY_ALLOW_EMBEDDING=true
|
|
64
|
+
volumes:
|
|
65
|
+
- grafana_data:/var/lib/grafana
|
|
66
|
+
|
|
67
|
+
node-exporter:
|
|
68
|
+
image: prom/node-exporter:v1.11.1
|
|
69
|
+
restart: unless-stopped
|
|
70
|
+
command:
|
|
71
|
+
- --path.rootfs=/host
|
|
72
|
+
volumes:
|
|
73
|
+
- /:/host:ro # read-only host metrics for the Node Health page
|
|
74
|
+
|
|
75
|
+
volumes:
|
|
76
|
+
prometheus_data:
|
|
77
|
+
grafana_data:
|
|
@@ -53,7 +53,7 @@ module.exports = function registerCheckRelayerHealth(server, options = {}) {
|
|
|
53
53
|
|
|
54
54
|
server.tool(
|
|
55
55
|
'check_relayer_health',
|
|
56
|
-
'Check the health of all Relayer services: UI (port 8888), S3 gateway (port 9000), and
|
|
56
|
+
'Check the health of all Relayer services: UI (port 8888), S3 gateway (port 9000), HostIO, and the monitoring sidecars (Prometheus + Grafana containers). Polls every 10 seconds for up to 300 seconds. Reports each component status individually and names any unhealthy component; a missing monitoring stack reports as degraded (dashboards empty) without blocking the install flow. Targets the machine the Docker daemon runs on (auto-detected from the Docker context — supports remote ssh:// Docker hosts); pass host to override. Note: HostIO health status is unknown until OIDC authentication is completed.',
|
|
57
57
|
{
|
|
58
58
|
poll: z.boolean().optional().default(true).describe('If true (default), poll until healthy or timeout. If false, check once.'),
|
|
59
59
|
host: z.string().trim().min(1).optional().describe('Hostname/IP where the Relayer containers run. Default: auto-detected from the Docker context (localhost, or the remote host for ssh:// / tcp:// contexts).'),
|
|
@@ -104,6 +104,25 @@ module.exports = function registerCheckRelayerHealth(server, options = {}) {
|
|
|
104
104
|
components.hostio = { healthy: null, status: null, note: 'HostIO health unknown — authentication not yet completed' };
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Monitoring sidecars (Prometheus + Grafana) — the channel
|
|
108
|
+
// bundle ships them; without them the dashboards under
|
|
109
|
+
// Monitoring in the web UI are dead. Absence DEGRADES the
|
|
110
|
+
// install (surfaced + named) but never blocks the core flow
|
|
111
|
+
// — claim/onboarding doesn't depend on monitoring.
|
|
112
|
+
const [prometheus, grafana] = await Promise.all([
|
|
113
|
+
docker.isContainerRunning('prometheus'),
|
|
114
|
+
docker.isContainerRunning('grafana'),
|
|
115
|
+
]);
|
|
116
|
+
const monitoringHealthy = prometheus === true && grafana === true;
|
|
117
|
+
components.monitoring = {
|
|
118
|
+
healthy: monitoringHealthy,
|
|
119
|
+
prometheus,
|
|
120
|
+
grafana,
|
|
121
|
+
...(monitoringHealthy ? {} : {
|
|
122
|
+
note: 'Monitoring stack (Prometheus/Grafana) is not running — the dashboards under Monitoring in the web UI will be empty. The channel bundle compose includes both; re-run docker compose up -d with the channel bundle to add them.',
|
|
123
|
+
}),
|
|
124
|
+
};
|
|
125
|
+
|
|
107
126
|
// Healthy if UI and S3 are up. HostIO null (unknown) does NOT block.
|
|
108
127
|
const coreHealthy = components.ui.healthy === true && components.s3.healthy === true;
|
|
109
128
|
const allKnownHealthy = coreHealthy && components.hostio.healthy === true;
|
|
@@ -116,16 +135,24 @@ module.exports = function registerCheckRelayerHealth(server, options = {}) {
|
|
|
116
135
|
components,
|
|
117
136
|
healthy: coreHealthy,
|
|
118
137
|
all_healthy: allKnownHealthy,
|
|
138
|
+
// JSON.stringify drops undefined — degraded only appears when true.
|
|
139
|
+
degraded: monitoringHealthy ? undefined : true,
|
|
119
140
|
unhealthy: unhealthy.length > 0 ? unhealthy : undefined,
|
|
120
141
|
target_host: target,
|
|
121
142
|
remote_docker: dockerHost.remote === true || undefined,
|
|
122
143
|
};
|
|
123
144
|
};
|
|
124
145
|
|
|
146
|
+
// Degraded (monitoring down) is appended to ANY healthy message —
|
|
147
|
+
// success stays true, the gap is named instead of swallowed.
|
|
148
|
+
const degradedSuffix = (r) => (r.degraded
|
|
149
|
+
? ' DEGRADED: monitoring stack (Prometheus/Grafana) is not running — Monitoring dashboards will be empty.'
|
|
150
|
+
: '');
|
|
151
|
+
|
|
125
152
|
if (!poll) {
|
|
126
153
|
const result = await checkOnce();
|
|
127
154
|
const message = result.healthy
|
|
128
|
-
? 'Relayer core services (UI + S3) are healthy.' + (result.all_healthy ? ' HostIO is also healthy.' : ' HostIO status is pending authentication.')
|
|
155
|
+
? 'Relayer core services (UI + S3) are healthy.' + (result.all_healthy ? ' HostIO is also healthy.' : ' HostIO status is pending authentication.') + degradedSuffix(result)
|
|
129
156
|
: `Relayer is not yet healthy. Unhealthy: ${result.unhealthy.join(', ')}.`;
|
|
130
157
|
|
|
131
158
|
return {
|
|
@@ -163,9 +190,9 @@ module.exports = function registerCheckRelayerHealth(server, options = {}) {
|
|
|
163
190
|
};
|
|
164
191
|
}
|
|
165
192
|
|
|
166
|
-
const message = result.all_healthy
|
|
193
|
+
const message = (result.all_healthy
|
|
167
194
|
? 'All Relayer services are healthy (UI, S3, HostIO). Ready for claim.'
|
|
168
|
-
: 'Relayer core services (UI + S3) are healthy. HostIO status pending authentication. Proceed to start_claim.';
|
|
195
|
+
: 'Relayer core services (UI + S3) are healthy. HostIO status pending authentication. Proceed to start_claim.') + degradedSuffix(result);
|
|
169
196
|
|
|
170
197
|
return {
|
|
171
198
|
content: [{
|
|
@@ -4,8 +4,14 @@ const { z } = require('zod');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { createDockerUtil } = require('../lib/dockerUtil');
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// Canonical released install — the full beta channel bundle (relayer +
|
|
8
|
+
// monitoring stack). Versioned in the deploy repo, shipped to web01 by
|
|
9
|
+
// `deploy.py promote`, served login-free. THE default install source.
|
|
10
|
+
const CHANNEL_COMPOSE_URL = 'https://releases.scpri.me/relayer/beta/docker-compose.yml';
|
|
11
|
+
|
|
12
|
+
// Bundled OFFLINE FALLBACK template (ships in the npm package; package.json
|
|
13
|
+
// `files: ["src/"]` covers it). Written only when the channel fetch fails;
|
|
14
|
+
// kept service-parity with the channel bundle by the jest contract tests.
|
|
9
15
|
const TEMPLATE_PATH = path.join(__dirname, '..', 'templates', 'docker-compose.yml');
|
|
10
16
|
|
|
11
17
|
// container_name in the bundled compose. Docker container names are unique
|
|
@@ -19,11 +25,12 @@ const CONTAINER_NAME = 'xns-relayer';
|
|
|
19
25
|
* TP-30: uses execFile, no shell (security non-negotiable).
|
|
20
26
|
*
|
|
21
27
|
* A first-time user has never heard of a Relayer and cannot supply a compose
|
|
22
|
-
* file or a .env. So by default this tool
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
28
|
+
* file or a .env. So by default this tool authors both for them: it fetches
|
|
29
|
+
* the CANONICAL channel bundle (relayer + Prometheus + Grafana + node-exporter
|
|
30
|
+
* — the monitoring stack powers the dashboards under Monitoring in the web UI)
|
|
31
|
+
* and writes a .env carrying the two ports. If the fetch fails (offline,
|
|
32
|
+
* registry hiccup), the bundled service-parity template is the fallback — the
|
|
33
|
+
* install still completes and the response says it fell back.
|
|
27
34
|
*
|
|
28
35
|
* `compose_url` stays as an optional override for internal/custom installs;
|
|
29
36
|
* when given, the old download-a-URL behaviour is preserved.
|
|
@@ -35,7 +42,7 @@ module.exports = function registerInstallRelayer(server, options = {}) {
|
|
|
35
42
|
|
|
36
43
|
server.tool(
|
|
37
44
|
'install_relayer',
|
|
38
|
-
'Install and start the XNS Relayer. By default
|
|
45
|
+
'Install and start the XNS Relayer. By default fetches the canonical beta channel bundle — relayer + the Prometheus/Grafana monitoring stack — from releases.scpri.me (anonymous pull) and writes a .env, then runs docker compose up -d — the user does NOT need to author any file. Falls back to a bundled copy of the bundle if the fetch fails. Pass compose_url only to override with a custom compose.',
|
|
39
46
|
{
|
|
40
47
|
install_path: z.string().optional().default('/opt/xns-relayer').describe('Directory to install the compose file into'),
|
|
41
48
|
ui_port: z.number().int().positive().optional().default(8888).describe('Host port for the Relayer admin/customer UI (container 8888)'),
|
|
@@ -77,18 +84,33 @@ module.exports = function registerInstallRelayer(server, options = {}) {
|
|
|
77
84
|
});
|
|
78
85
|
});
|
|
79
86
|
|
|
87
|
+
const fetchCompose = (url) => new Promise((resolve, reject) => {
|
|
88
|
+
execFileFn('curl', ['-fsSL', '-o', composePath, url], { timeout: 60000 }, (err) => {
|
|
89
|
+
if (err) return reject(new Error(`Failed to download compose file: ${err.message}`));
|
|
90
|
+
resolve();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
let source;
|
|
95
|
+
let note;
|
|
80
96
|
if (compose_url) {
|
|
81
97
|
// Override path: download a custom compose (execFile, no shell).
|
|
82
|
-
await
|
|
83
|
-
|
|
84
|
-
if (err) return reject(new Error(`Failed to download compose file: ${err.message}`));
|
|
85
|
-
resolve();
|
|
86
|
-
});
|
|
87
|
-
});
|
|
98
|
+
await fetchCompose(compose_url);
|
|
99
|
+
source = 'compose_url';
|
|
88
100
|
} else {
|
|
89
|
-
// Default path:
|
|
90
|
-
|
|
91
|
-
|
|
101
|
+
// Default path: fetch the canonical channel bundle (relayer +
|
|
102
|
+
// monitoring stack); fall back to the bundled service-parity
|
|
103
|
+
// template only when the fetch fails. Either way, author the
|
|
104
|
+
// .env so the user never writes a file.
|
|
105
|
+
try {
|
|
106
|
+
await fetchCompose(CHANNEL_COMPOSE_URL);
|
|
107
|
+
source = 'channel';
|
|
108
|
+
} catch (fetchErr) {
|
|
109
|
+
const template = await fsp.readFile(TEMPLATE_PATH, 'utf8');
|
|
110
|
+
await fsp.writeFile(composePath, template);
|
|
111
|
+
source = 'bundled-fallback';
|
|
112
|
+
note = `Channel bundle fetch failed (${fetchErr.message}) — fell back to the bundled compose. Same services; re-running install later is not required.`;
|
|
113
|
+
}
|
|
92
114
|
await fsp.writeFile(envPath, `UI_PORT=${ui_port}\nS3_PORT=${s3_port}\n`);
|
|
93
115
|
}
|
|
94
116
|
|
|
@@ -108,7 +130,8 @@ module.exports = function registerInstallRelayer(server, options = {}) {
|
|
|
108
130
|
message: 'XNS Relayer containers are starting. Use check_relayer_health to monitor when all services are ready.',
|
|
109
131
|
compose_path: composePath,
|
|
110
132
|
install_path,
|
|
111
|
-
source
|
|
133
|
+
source,
|
|
134
|
+
...(note ? { note } : {}),
|
|
112
135
|
}, null, 2),
|
|
113
136
|
}],
|
|
114
137
|
};
|