@xns-cloud/relayer-mcp 0.5.0 → 0.5.1
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 +1 -1
- package/package.json +1 -1
- package/src/lib/s3Client.js +19 -0
- package/src/tools/installRelayer.js +2 -1
- package/src/tools/verifyStorage.js +76 -27
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ No separate install step required.
|
|
|
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. |
|
|
54
54
|
| 9 | `configure_vpd` | Set data/parity host selection via CEL expressions. |
|
|
55
|
-
| 10 | `verify_storage` | Round-trip S3 test (create bucket, put object, get object) against the S3 gateway on port 9000. |
|
|
55
|
+
| 10 | `verify_storage` | Round-trip S3 test (create bucket, put object, get object) against the S3 gateway on port 9000. **Requires fullaccess credentials** (admin key pair from the Relayer UI IAM page — not a read-only or bucket-scoped key). Test bucket is cleaned up automatically. If auto-detection cannot reach the host, pass `endpoint` with an explicit IP (e.g. `http://192.168.1.100:9000`). |
|
|
56
56
|
| 11 | `setup_cli_credentials` | Provision S3 IAM credentials and write `~/.xns/credentials` so the XNS CLI works without further configuration. |
|
|
57
57
|
|
|
58
58
|
## Onboarding Flow
|
package/package.json
CHANGED
package/src/lib/s3Client.js
CHANGED
|
@@ -5,6 +5,8 @@ const {
|
|
|
5
5
|
CreateBucketCommand,
|
|
6
6
|
PutObjectCommand,
|
|
7
7
|
GetObjectCommand,
|
|
8
|
+
DeleteObjectCommand,
|
|
9
|
+
DeleteBucketCommand,
|
|
8
10
|
} = require('@aws-sdk/client-s3');
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -65,6 +67,23 @@ function createS3Client(options = {}) {
|
|
|
65
67
|
return resp.Body.transformToString();
|
|
66
68
|
},
|
|
67
69
|
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} bucket
|
|
72
|
+
* @param {string} key
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
async deleteObject(bucket, key) {
|
|
76
|
+
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {string} bucket
|
|
81
|
+
* @returns {Promise<void>}
|
|
82
|
+
*/
|
|
83
|
+
async deleteBucket(bucket) {
|
|
84
|
+
await client.send(new DeleteBucketCommand({ Bucket: bucket }));
|
|
85
|
+
},
|
|
86
|
+
|
|
68
87
|
/** Expose the raw client for advanced use */
|
|
69
88
|
client,
|
|
70
89
|
};
|
|
@@ -39,6 +39,7 @@ module.exports = function registerInstallRelayer(server, options = {}) {
|
|
|
39
39
|
const docker = options.dockerUtil || createDockerUtil(options);
|
|
40
40
|
const _execFile = options.execFile;
|
|
41
41
|
const fsp = options.fs || require('fs').promises;
|
|
42
|
+
const channelComposeUrl = options.channelComposeUrl || CHANNEL_COMPOSE_URL;
|
|
42
43
|
|
|
43
44
|
server.tool(
|
|
44
45
|
'install_relayer',
|
|
@@ -103,7 +104,7 @@ module.exports = function registerInstallRelayer(server, options = {}) {
|
|
|
103
104
|
// template only when the fetch fails. Either way, author the
|
|
104
105
|
// .env so the user never writes a file.
|
|
105
106
|
try {
|
|
106
|
-
await fetchCompose(
|
|
107
|
+
await fetchCompose(channelComposeUrl);
|
|
107
108
|
source = 'channel';
|
|
108
109
|
} catch (fetchErr) {
|
|
109
110
|
const template = await fsp.readFile(TEMPLATE_PATH, 'utf8');
|
|
@@ -8,6 +8,10 @@ const { createDockerUtil } = require('../lib/dockerUtil');
|
|
|
8
8
|
* Tool 10: verify_storage
|
|
9
9
|
* AC-20: targets the S3 gateway on port 9000 plain HTTP; reports endpoint.
|
|
10
10
|
* AC-21: verify fail → names the failing step (CreateBucket/PutObject/GetObject).
|
|
11
|
+
* W3-AC1: cleanup in finally — always removes mcp-verify-* bucket/object; failure
|
|
12
|
+
* is non-fatal (noted in response, never flips pass→fail).
|
|
13
|
+
* W3-AC2: fullaccess credential requirement stated in description and error output.
|
|
14
|
+
* W3-AC3: explicit-IP hint when endpoint was auto-detected.
|
|
11
15
|
* R6: S3 credentials must be provided (from relayer-ui IAM key-mgmt flow).
|
|
12
16
|
*
|
|
13
17
|
* Performs a round-trip verification: CreateBucket → PutObject → GetObject → compare.
|
|
@@ -20,22 +24,26 @@ module.exports = function registerVerifyStorage(server, options = {}) {
|
|
|
20
24
|
|
|
21
25
|
server.tool(
|
|
22
26
|
'verify_storage',
|
|
23
|
-
'Verify the S3-compatible storage gateway is working by performing a round-trip test: create a test bucket, upload a small object, download it, and compare. By default targets port 9000 on the machine the Docker daemon runs on (auto-detected from the Docker context — supports remote ssh:// Docker hosts); pass endpoint to override
|
|
27
|
+
'Verify the S3-compatible storage gateway is working by performing a round-trip test: create a test bucket, upload a small object, download it, and compare. IMPORTANT: you must supply fullaccess credentials — the admin key pair created via the Relayer UI IAM page (not a read-only or bucket-scoped key). By default targets port 9000 on the machine the Docker daemon runs on (auto-detected from the Docker context — supports remote ssh:// Docker hosts); pass endpoint to override with an explicit IP when auto-detection cannot reach the host.',
|
|
24
28
|
{
|
|
25
|
-
access_key_id: z.string().describe('S3 access key ID'),
|
|
26
|
-
secret_access_key: z.string().describe('S3 secret access key'),
|
|
27
|
-
endpoint: z.string().trim().url().optional().describe('S3 endpoint URL. Default: http://{docker-host}:9000, where {docker-host} is auto-detected from the Docker context.'),
|
|
29
|
+
access_key_id: z.string().describe('S3 access key ID (fullaccess credentials from the Relayer UI IAM page)'),
|
|
30
|
+
secret_access_key: z.string().describe('S3 secret access key (fullaccess credentials from the Relayer UI IAM page)'),
|
|
31
|
+
endpoint: z.string().trim().url().optional().describe('S3 endpoint URL. Default: http://{docker-host}:9000, where {docker-host} is auto-detected from the Docker context. Pass an explicit IP (e.g. http://192.168.1.100:9000) when auto-detection cannot reach the host.'),
|
|
28
32
|
},
|
|
29
33
|
async ({ access_key_id, secret_access_key, endpoint }) => {
|
|
30
34
|
const testBucket = `mcp-verify-${Date.now()}`;
|
|
31
35
|
const testKey = 'verify-test.txt';
|
|
32
36
|
const testContent = `relayer-mcp verification ${Date.now()}`;
|
|
33
37
|
let currentStep = 'init';
|
|
38
|
+
let endpointAutoDetected = false;
|
|
39
|
+
|
|
40
|
+
let verifyResult = null;
|
|
34
41
|
|
|
35
42
|
try {
|
|
36
43
|
if (!endpoint) {
|
|
37
44
|
const { host } = await docker.getDockerHost();
|
|
38
45
|
endpoint = `http://${host}:9000`;
|
|
46
|
+
endpointAutoDetected = true;
|
|
39
47
|
}
|
|
40
48
|
const s3 = _createS3Client({
|
|
41
49
|
endpoint,
|
|
@@ -58,42 +66,83 @@ module.exports = function registerVerifyStorage(server, options = {}) {
|
|
|
58
66
|
// Step 4: Compare
|
|
59
67
|
currentStep = 'Compare';
|
|
60
68
|
if (retrieved !== testContent) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
verifyResult = {
|
|
70
|
+
success: false,
|
|
71
|
+
error: 'Storage verification failed at Compare: uploaded content does not match downloaded content. The S3 gateway may have data integrity issues.',
|
|
72
|
+
endpoint,
|
|
73
|
+
failing_step: 'Compare',
|
|
74
|
+
};
|
|
75
|
+
} else {
|
|
76
|
+
const explicitIpHint = endpointAutoDetected
|
|
77
|
+
? `If auto-detection cannot reach the host, pass an explicit endpoint: endpoint option (e.g. "http://<ip>:9000").`
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
verifyResult = {
|
|
81
|
+
success: true,
|
|
82
|
+
message: `S3 storage verification passed. Successfully created bucket, uploaded object, and verified download at ${endpoint}.`,
|
|
83
|
+
endpoint,
|
|
84
|
+
test_bucket: testBucket,
|
|
85
|
+
...(explicitIpHint ? { note: explicitIpHint } : {}),
|
|
72
86
|
};
|
|
73
87
|
}
|
|
74
88
|
|
|
89
|
+
// Cleanup: remove test object and bucket (W3-AC1)
|
|
90
|
+
try {
|
|
91
|
+
await s3.deleteObject(testBucket, testKey);
|
|
92
|
+
await s3.deleteBucket(testBucket);
|
|
93
|
+
} catch (cleanupErr) {
|
|
94
|
+
// Non-fatal: note it but never flip the result
|
|
95
|
+
verifyResult.cleanup_warning = `Test bucket cleanup failed (${cleanupErr.message}). The bucket "${testBucket}" may remain — remove it manually via the Relayer UI.`;
|
|
96
|
+
}
|
|
97
|
+
|
|
75
98
|
return {
|
|
76
99
|
content: [{
|
|
77
100
|
type: 'text',
|
|
78
|
-
text: JSON.stringify(
|
|
79
|
-
success: true,
|
|
80
|
-
message: `S3 storage verification passed. Successfully created bucket, uploaded object, and verified download at ${endpoint}.`,
|
|
81
|
-
endpoint,
|
|
82
|
-
test_bucket: testBucket,
|
|
83
|
-
}, null, 2),
|
|
101
|
+
text: JSON.stringify(verifyResult, null, 2),
|
|
84
102
|
}],
|
|
103
|
+
...(verifyResult.success ? {} : { isError: true }),
|
|
85
104
|
};
|
|
86
105
|
} catch (err) {
|
|
106
|
+
// Attempt cleanup even on error (W3-AC1)
|
|
107
|
+
let cleanupWarning;
|
|
108
|
+
if (currentStep !== 'init' && currentStep !== 'CreateBucket') {
|
|
109
|
+
// Bucket was created before the failure — try to clean up
|
|
110
|
+
try {
|
|
111
|
+
const s3Cleanup = _createS3Client({
|
|
112
|
+
endpoint,
|
|
113
|
+
accessKeyId: access_key_id,
|
|
114
|
+
secretAccessKey: secret_access_key,
|
|
115
|
+
});
|
|
116
|
+
await s3Cleanup.deleteObject(testBucket, testKey).catch(() => {});
|
|
117
|
+
await s3Cleanup.deleteBucket(testBucket);
|
|
118
|
+
} catch (cleanupErr) {
|
|
119
|
+
cleanupWarning = `Test bucket cleanup failed (${cleanupErr.message}). The bucket "${testBucket}" may remain — remove it manually.`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const isCredentialError = /AccessDenied|InvalidAccessKeyId|SignatureDoesNotMatch|Forbidden|401|403/i.test(err.message);
|
|
124
|
+
const credentialHint = isCredentialError
|
|
125
|
+
? ' Ensure you are using fullaccess credentials from the Relayer UI IAM page (not a read-only or bucket-scoped key).'
|
|
126
|
+
: '';
|
|
127
|
+
|
|
128
|
+
const explicitIpHint = endpointAutoDetected
|
|
129
|
+
? ` If the endpoint cannot be reached, pass an explicit IP via the endpoint option (e.g. "http://<ip>:9000").`
|
|
130
|
+
: '';
|
|
131
|
+
|
|
87
132
|
// AC-21: name the failing step
|
|
133
|
+
const errorPayload = {
|
|
134
|
+
success: false,
|
|
135
|
+
error: `Storage verification failed at ${currentStep}: ${err.message}.${credentialHint}${explicitIpHint}`,
|
|
136
|
+
endpoint,
|
|
137
|
+
failing_step: currentStep,
|
|
138
|
+
...(isCredentialError ? { credential_requirement: 'fullaccess credentials required — use the admin key pair from the Relayer UI IAM page.' } : {}),
|
|
139
|
+
...(cleanupWarning ? { cleanup_warning: cleanupWarning } : {}),
|
|
140
|
+
};
|
|
141
|
+
|
|
88
142
|
return {
|
|
89
143
|
content: [{
|
|
90
144
|
type: 'text',
|
|
91
|
-
text: JSON.stringify(
|
|
92
|
-
success: false,
|
|
93
|
-
error: `Storage verification failed at ${currentStep}: ${err.message}`,
|
|
94
|
-
endpoint,
|
|
95
|
-
failing_step: currentStep,
|
|
96
|
-
}),
|
|
145
|
+
text: JSON.stringify(errorPayload),
|
|
97
146
|
}],
|
|
98
147
|
isError: true,
|
|
99
148
|
};
|