@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xns-cloud/relayer-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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",
@@ -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(CHANNEL_COMPOSE_URL);
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. You must provide S3 access credentials these can be found in the Relayer configuration or generated via the Relayer UI.',
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
- return {
62
- content: [{
63
- type: 'text',
64
- text: JSON.stringify({
65
- success: false,
66
- error: 'Storage verification failed at Compare: uploaded content does not match downloaded content. The S3 gateway may have data integrity issues.',
67
- endpoint,
68
- failing_step: 'Compare',
69
- }),
70
- }],
71
- isError: true,
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
  };