@aws/ml-container-creator 0.9.1 → 0.10.3
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/LICENSE-THIRD-PARTY +9304 -0
- package/bin/cli.js +2 -0
- package/config/bootstrap-e2e-stack.json +341 -0
- package/config/bootstrap-stack.json +40 -3
- package/config/parameter-schema-v2.json +2049 -0
- package/config/tune-catalog.json +1781 -0
- package/infra/ci-harness/buildspec.yml +1 -0
- package/infra/ci-harness/lambda/path-prover/brain.ts +306 -0
- package/infra/ci-harness/lambda/path-prover/write-results.ts +152 -0
- package/infra/ci-harness/lib/ci-harness-stack.ts +837 -7
- package/infra/ci-harness/state-machines/path-prover.asl.json +496 -0
- package/package.json +53 -68
- package/servers/base-image-picker/index.js +121 -121
- package/servers/e2e-status/index.js +297 -0
- package/servers/e2e-status/manifest.json +14 -0
- package/servers/e2e-status/package.json +15 -0
- package/servers/endpoint-picker/LICENSE +202 -0
- package/servers/endpoint-picker/index.js +536 -0
- package/servers/endpoint-picker/manifest.json +14 -0
- package/servers/endpoint-picker/package.json +18 -0
- package/servers/hyperpod-cluster-picker/index.js +125 -125
- package/servers/instance-sizer/index.js +138 -138
- package/servers/instance-sizer/lib/instance-ranker.js +76 -76
- package/servers/instance-sizer/lib/model-resolver.js +61 -61
- package/servers/instance-sizer/lib/quota-resolver.js +113 -113
- package/servers/instance-sizer/lib/vram-estimator.js +31 -31
- package/servers/lib/bedrock-client.js +38 -38
- package/servers/lib/catalogs/jumpstart-public.json +101 -16
- package/servers/lib/catalogs/model-servers.json +201 -3
- package/servers/lib/catalogs/models.json +182 -26
- package/servers/lib/custom-validators.js +13 -13
- package/servers/lib/dynamic-resolver.js +4 -4
- package/servers/marketplace-picker/index.js +342 -0
- package/servers/marketplace-picker/manifest.json +14 -0
- package/servers/marketplace-picker/package.json +18 -0
- package/servers/model-picker/index.js +382 -382
- package/servers/region-picker/index.js +56 -56
- package/servers/workload-picker/LICENSE +202 -0
- package/servers/workload-picker/catalogs/workload-profiles.json +67 -0
- package/servers/workload-picker/index.js +171 -0
- package/servers/workload-picker/manifest.json +16 -0
- package/servers/workload-picker/package.json +16 -0
- package/src/app.js +4 -390
- package/src/lib/bootstrap-command-handler.js +710 -1148
- package/src/lib/bootstrap-config.js +36 -0
- package/src/lib/bootstrap-profile-manager.js +641 -0
- package/src/lib/bootstrap-provisioners.js +421 -0
- package/src/lib/ci-register-helpers.js +74 -0
- package/src/lib/config-loader.js +408 -0
- package/src/lib/config-manager.js +66 -1685
- package/src/lib/config-mcp-client.js +118 -0
- package/src/lib/config-validator.js +634 -0
- package/src/lib/cuda-resolver.js +149 -0
- package/src/lib/e2e-catalog-validator.js +251 -3
- package/src/lib/e2e-ci-recorder.js +103 -0
- package/src/lib/generated/cli-options.js +315 -311
- package/src/lib/generated/parameter-matrix.js +671 -0
- package/src/lib/generated/validation-rules.js +71 -71
- package/src/lib/marketplace-flow.js +276 -0
- package/src/lib/mcp-query-runner.js +768 -0
- package/src/lib/parameter-schema-validator.js +62 -18
- package/src/lib/path-prover-brain.js +607 -0
- package/src/lib/prompt-runner.js +41 -1504
- package/src/lib/prompts/feature-prompts.js +172 -0
- package/src/lib/prompts/index.js +48 -0
- package/src/lib/prompts/infrastructure-prompts.js +690 -0
- package/src/lib/prompts/model-prompts.js +552 -0
- package/src/lib/prompts/project-prompts.js +82 -0
- package/src/lib/prompts.js +2 -1446
- package/src/lib/registry-command-handler.js +135 -3
- package/src/lib/secrets-prompt-runner.js +251 -0
- package/src/lib/template-variable-resolver.js +422 -0
- package/src/lib/tune-catalog-validator.js +37 -4
- package/templates/Dockerfile +9 -0
- package/templates/code/adapter_sidecar.py +444 -0
- package/templates/code/serve +6 -0
- package/templates/code/serve.d/vllm.ejs +1 -1
- package/templates/do/.benchmark_writer.py +1476 -0
- package/templates/do/.tune_helper.py +982 -57
- package/templates/do/__pycache__/.benchmark_writer.cpython-312.pyc +0 -0
- package/templates/do/adapter +149 -0
- package/templates/do/benchmark +639 -85
- package/templates/do/config +108 -5
- package/templates/do/deploy.d/managed-inference.ejs +192 -11
- package/templates/do/optimize +106 -37
- package/templates/do/register +89 -0
- package/templates/do/test +13 -0
- package/templates/do/tune +378 -59
- package/templates/do/validate +44 -4
- package/config/parameter-schema.json +0 -88
|
@@ -19,20 +19,20 @@
|
|
|
19
19
|
* AWS_REGION - AWS region for SageMaker API calls (default: us-east-1)
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
23
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
24
|
-
import { z } from 'zod'
|
|
25
|
-
import { fileURLToPath } from 'node:url'
|
|
26
|
-
import { resolve } from 'node:path'
|
|
27
|
-
import { readFileSync } from 'node:fs'
|
|
28
|
-
import { homedir } from 'node:os'
|
|
29
|
-
import { DynamicResolver } from '../lib/dynamic-resolver.js'
|
|
22
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
23
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
24
|
+
import { z } from 'zod';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
import { resolve } from 'node:path';
|
|
27
|
+
import { readFileSync } from 'node:fs';
|
|
28
|
+
import { homedir } from 'node:os';
|
|
29
|
+
import { DynamicResolver } from '../lib/dynamic-resolver.js';
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Log to stderr so it doesn't interfere with MCP stdio protocol on stdout.
|
|
33
33
|
*/
|
|
34
34
|
function log(message) {
|
|
35
|
-
process.stderr.write(`[hyperpod-cluster-picker] ${message}\n`)
|
|
35
|
+
process.stderr.write(`[hyperpod-cluster-picker] ${message}\n`);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
@@ -46,14 +46,14 @@ function log(message) {
|
|
|
46
46
|
* @returns {object} SageMaker client
|
|
47
47
|
*/
|
|
48
48
|
function createSageMakerClient(region, clientFactory = null) {
|
|
49
|
-
if (clientFactory) return clientFactory(region)
|
|
50
|
-
return _defaultClientFactory(region)
|
|
49
|
+
if (clientFactory) return clientFactory(region);
|
|
50
|
+
return _defaultClientFactory(region);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
let _SageMakerClient = null
|
|
54
|
-
let _ListClustersCommand = null
|
|
55
|
-
let _DescribeClusterCommand = null
|
|
56
|
-
let _fromIni = null
|
|
53
|
+
let _SageMakerClient = null;
|
|
54
|
+
let _ListClustersCommand = null;
|
|
55
|
+
let _DescribeClusterCommand = null;
|
|
56
|
+
let _fromIni = null;
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Lazily load the AWS SDK SageMaker client classes.
|
|
@@ -61,21 +61,21 @@ let _fromIni = null
|
|
|
61
61
|
* without requiring @aws-sdk/client-sagemaker to be installed.
|
|
62
62
|
*/
|
|
63
63
|
async function _ensureSdkLoaded() {
|
|
64
|
-
if (_SageMakerClient) return
|
|
65
|
-
const sdk = await import('@aws-sdk/client-sagemaker')
|
|
66
|
-
_SageMakerClient = sdk.SageMakerClient
|
|
67
|
-
_ListClustersCommand = sdk.ListClustersCommand
|
|
68
|
-
_DescribeClusterCommand = sdk.DescribeClusterCommand
|
|
64
|
+
if (_SageMakerClient) return;
|
|
65
|
+
const sdk = await import('@aws-sdk/client-sagemaker');
|
|
66
|
+
_SageMakerClient = sdk.SageMakerClient;
|
|
67
|
+
_ListClustersCommand = sdk.ListClustersCommand;
|
|
68
|
+
_DescribeClusterCommand = sdk.DescribeClusterCommand;
|
|
69
69
|
try {
|
|
70
|
-
const credentialProviders = await import('@aws-sdk/credential-providers')
|
|
71
|
-
_fromIni = credentialProviders.fromIni
|
|
70
|
+
const credentialProviders = await import('@aws-sdk/credential-providers');
|
|
71
|
+
_fromIni = credentialProviders.fromIni;
|
|
72
72
|
} catch {
|
|
73
73
|
// credential-providers not available — profile-based fallback won't work
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function _defaultClientFactory(region) {
|
|
78
|
-
return new _SageMakerClient({ region })
|
|
78
|
+
return new _SageMakerClient({ region });
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
@@ -86,12 +86,12 @@ function _defaultClientFactory(region) {
|
|
|
86
86
|
*/
|
|
87
87
|
function _createClientWithProfile(region, profile) {
|
|
88
88
|
if (!_fromIni) {
|
|
89
|
-
throw new Error('Cannot use profile-based credentials: @aws-sdk/credential-providers not available')
|
|
89
|
+
throw new Error('Cannot use profile-based credentials: @aws-sdk/credential-providers not available');
|
|
90
90
|
}
|
|
91
91
|
return new _SageMakerClient({
|
|
92
92
|
region,
|
|
93
93
|
credentials: _fromIni({ profile })
|
|
94
|
-
})
|
|
94
|
+
});
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
@@ -99,22 +99,22 @@ function _createClientWithProfile(region, profile) {
|
|
|
99
99
|
* @returns {string[]} Array of profile names
|
|
100
100
|
*/
|
|
101
101
|
function _detectAwsProfiles() {
|
|
102
|
-
const profiles = new Set()
|
|
102
|
+
const profiles = new Set();
|
|
103
103
|
try {
|
|
104
|
-
const credsPath = resolve(homedir(), '.aws/credentials')
|
|
105
|
-
const creds = readFileSync(credsPath, 'utf8')
|
|
104
|
+
const credsPath = resolve(homedir(), '.aws/credentials');
|
|
105
|
+
const creds = readFileSync(credsPath, 'utf8');
|
|
106
106
|
for (const match of creds.matchAll(/^\[(.+)\]$/gm)) {
|
|
107
|
-
profiles.add(match[1])
|
|
107
|
+
profiles.add(match[1]);
|
|
108
108
|
}
|
|
109
109
|
} catch { /* no credentials file */ }
|
|
110
110
|
try {
|
|
111
|
-
const configPath = resolve(homedir(), '.aws/config')
|
|
112
|
-
const config = readFileSync(configPath, 'utf8')
|
|
111
|
+
const configPath = resolve(homedir(), '.aws/config');
|
|
112
|
+
const config = readFileSync(configPath, 'utf8');
|
|
113
113
|
for (const match of config.matchAll(/^\[profile\s+(.+)\]$/gm)) {
|
|
114
|
-
profiles.add(match[1])
|
|
114
|
+
profiles.add(match[1]);
|
|
115
115
|
}
|
|
116
116
|
} catch { /* no config file */ }
|
|
117
|
-
return [...profiles]
|
|
117
|
+
return [...profiles];
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
@@ -125,65 +125,65 @@ function _detectAwsProfiles() {
|
|
|
125
125
|
* @returns {Promise<Array<{ clusterName: string, clusterArn: string, status: string, instanceGroups: Array }>>}
|
|
126
126
|
*/
|
|
127
127
|
async function fetchHyperPodClusters(client, { limit = 10 } = {}) {
|
|
128
|
-
const clusters = []
|
|
129
|
-
let nextToken
|
|
128
|
+
const clusters = [];
|
|
129
|
+
let nextToken;
|
|
130
130
|
|
|
131
131
|
// Paginate through ListClusters
|
|
132
132
|
do {
|
|
133
|
-
const params = { MaxResults: 100 }
|
|
134
|
-
if (nextToken) params.NextToken = nextToken
|
|
133
|
+
const params = { MaxResults: 100 };
|
|
134
|
+
if (nextToken) params.NextToken = nextToken;
|
|
135
135
|
|
|
136
|
-
const command = new _ListClustersCommand(params)
|
|
137
|
-
const response = await client.send(command)
|
|
136
|
+
const command = new _ListClustersCommand(params);
|
|
137
|
+
const response = await client.send(command);
|
|
138
138
|
|
|
139
|
-
const summaries = response.ClusterSummaries || []
|
|
139
|
+
const summaries = response.ClusterSummaries || [];
|
|
140
140
|
for (const summary of summaries) {
|
|
141
141
|
// Filter: InService only
|
|
142
|
-
if (summary.ClusterStatus !== 'InService') continue
|
|
142
|
+
if (summary.ClusterStatus !== 'InService') continue;
|
|
143
143
|
|
|
144
144
|
clusters.push({
|
|
145
145
|
clusterName: summary.ClusterName,
|
|
146
146
|
clusterArn: summary.ClusterArn,
|
|
147
147
|
status: summary.ClusterStatus
|
|
148
|
-
})
|
|
148
|
+
});
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
nextToken = response.NextToken
|
|
152
|
-
} while (nextToken && clusters.length < limit * 3) // over-fetch to account for EKS filtering
|
|
151
|
+
nextToken = response.NextToken;
|
|
152
|
+
} while (nextToken && clusters.length < limit * 3); // over-fetch to account for EKS filtering
|
|
153
153
|
|
|
154
154
|
// Now describe each cluster to check orchestrator type and get instance groups
|
|
155
|
-
const eksClusters = []
|
|
155
|
+
const eksClusters = [];
|
|
156
156
|
for (const cluster of clusters) {
|
|
157
|
-
if (eksClusters.length >= limit) break
|
|
157
|
+
if (eksClusters.length >= limit) break;
|
|
158
158
|
|
|
159
159
|
try {
|
|
160
160
|
const describeCommand = new _DescribeClusterCommand({
|
|
161
161
|
ClusterName: cluster.clusterName
|
|
162
|
-
})
|
|
163
|
-
const detail = await client.send(describeCommand)
|
|
162
|
+
});
|
|
163
|
+
const detail = await client.send(describeCommand);
|
|
164
164
|
|
|
165
165
|
// Filter: EKS orchestrator only (exclude Slurm)
|
|
166
|
-
const orchestrator = detail.Orchestrator?.Eks ? 'EKS' : 'Slurm'
|
|
167
|
-
if (orchestrator !== 'EKS') continue
|
|
166
|
+
const orchestrator = detail.Orchestrator?.Eks ? 'EKS' : 'Slurm';
|
|
167
|
+
if (orchestrator !== 'EKS') continue;
|
|
168
168
|
|
|
169
169
|
const instanceGroups = (detail.InstanceGroups || []).map(g => ({
|
|
170
170
|
name: g.InstanceGroupName,
|
|
171
171
|
instanceType: g.InstanceType,
|
|
172
172
|
count: g.CurrentCount ?? g.TargetCount ?? 0
|
|
173
|
-
}))
|
|
173
|
+
}));
|
|
174
174
|
|
|
175
175
|
eksClusters.push({
|
|
176
176
|
clusterName: cluster.clusterName,
|
|
177
177
|
clusterArn: cluster.clusterArn,
|
|
178
178
|
status: cluster.status,
|
|
179
179
|
instanceGroups
|
|
180
|
-
})
|
|
180
|
+
});
|
|
181
181
|
} catch (err) {
|
|
182
|
-
log(`Warning: could not describe cluster "${cluster.clusterName}": ${err.message}`)
|
|
182
|
+
log(`Warning: could not describe cluster "${cluster.clusterName}": ${err.message}`);
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
return eksClusters
|
|
186
|
+
return eksClusters;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
/**
|
|
@@ -198,10 +198,10 @@ function buildResponse(clusters) {
|
|
|
198
198
|
values: {},
|
|
199
199
|
choices: { hyperPodCluster: [] },
|
|
200
200
|
message: 'No InService HyperPod EKS clusters found in the specified region. Verify the region and that you have HyperPod EKS clusters provisioned.'
|
|
201
|
-
}
|
|
201
|
+
};
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
const clusterNames = clusters.map(c => c.clusterName)
|
|
204
|
+
const clusterNames = clusters.map(c => c.clusterName);
|
|
205
205
|
|
|
206
206
|
return {
|
|
207
207
|
values: { hyperPodCluster: clusterNames[0] },
|
|
@@ -213,7 +213,7 @@ function buildResponse(clusters) {
|
|
|
213
213
|
instanceGroups: c.instanceGroups
|
|
214
214
|
}])
|
|
215
215
|
)
|
|
216
|
-
}
|
|
216
|
+
};
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
// ── ClusterResolver ──────────────────────────────────────────────────────────
|
|
@@ -226,70 +226,70 @@ function buildResponse(clusters) {
|
|
|
226
226
|
*/
|
|
227
227
|
class ClusterResolver extends DynamicResolver {
|
|
228
228
|
constructor(options = {}) {
|
|
229
|
-
super()
|
|
230
|
-
this._region = options.region || process.env.AWS_REGION || 'us-east-1'
|
|
231
|
-
this._profile = options.profile || process.env.AWS_PROFILE || null
|
|
232
|
-
this._clientFactory = options.clientFactory || null
|
|
229
|
+
super();
|
|
230
|
+
this._region = options.region || process.env.AWS_REGION || 'us-east-1';
|
|
231
|
+
this._profile = options.profile || process.env.AWS_PROFILE || null;
|
|
232
|
+
this._clientFactory = options.clientFactory || null;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
async fetch(key, options = {}) {
|
|
236
|
-
const { limit = 10 } = options
|
|
236
|
+
const { limit = 10 } = options;
|
|
237
237
|
|
|
238
|
-
await _ensureSdkLoaded()
|
|
238
|
+
await _ensureSdkLoaded();
|
|
239
239
|
|
|
240
|
-
let clusters = null
|
|
241
|
-
let lastError = null
|
|
240
|
+
let clusters = null;
|
|
241
|
+
let lastError = null;
|
|
242
242
|
|
|
243
243
|
// Strategy 1: If a specific profile was requested, use it directly
|
|
244
244
|
if (this._profile) {
|
|
245
245
|
try {
|
|
246
|
-
const client = _createClientWithProfile(this._region, this._profile)
|
|
247
|
-
clusters = await fetchHyperPodClusters(client, { limit })
|
|
246
|
+
const client = _createClientWithProfile(this._region, this._profile);
|
|
247
|
+
clusters = await fetchHyperPodClusters(client, { limit });
|
|
248
248
|
} catch (err) {
|
|
249
|
-
log(`Profile "${this._profile}" failed: ${err.message}`)
|
|
250
|
-
lastError = err
|
|
249
|
+
log(`Profile "${this._profile}" failed: ${err.message}`);
|
|
250
|
+
lastError = err;
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
// Strategy 2: Try the default credential chain
|
|
255
255
|
if (!clusters) {
|
|
256
256
|
try {
|
|
257
|
-
const client = createSageMakerClient(this._region, this._clientFactory)
|
|
258
|
-
clusters = await fetchHyperPodClusters(client, { limit })
|
|
257
|
+
const client = createSageMakerClient(this._region, this._clientFactory);
|
|
258
|
+
clusters = await fetchHyperPodClusters(client, { limit });
|
|
259
259
|
} catch (err) {
|
|
260
|
-
log(`Default credential chain failed: ${err.message}`)
|
|
261
|
-
lastError = err
|
|
260
|
+
log(`Default credential chain failed: ${err.message}`);
|
|
261
|
+
lastError = err;
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
265
|
// Strategy 3: Detect available AWS profiles and try each
|
|
266
266
|
if (!clusters && _fromIni) {
|
|
267
|
-
const profiles = _detectAwsProfiles()
|
|
267
|
+
const profiles = _detectAwsProfiles();
|
|
268
268
|
for (const p of profiles) {
|
|
269
269
|
try {
|
|
270
|
-
const client = _createClientWithProfile(this._region, p)
|
|
271
|
-
clusters = await fetchHyperPodClusters(client, { limit })
|
|
272
|
-
log(`Profile "${p}" succeeded`)
|
|
273
|
-
break
|
|
270
|
+
const client = _createClientWithProfile(this._region, p);
|
|
271
|
+
clusters = await fetchHyperPodClusters(client, { limit });
|
|
272
|
+
log(`Profile "${p}" succeeded`);
|
|
273
|
+
break;
|
|
274
274
|
} catch (err) {
|
|
275
|
-
log(`Profile "${p}" failed: ${err.message}`)
|
|
276
|
-
lastError = err
|
|
275
|
+
log(`Profile "${p}" failed: ${err.message}`);
|
|
276
|
+
lastError = err;
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
if (!clusters) {
|
|
282
|
-
throw lastError || new Error('No AWS credentials available')
|
|
282
|
+
throw lastError || new Error('No AWS credentials available');
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
return {
|
|
286
286
|
items: clusters,
|
|
287
287
|
defaultItem: clusters[0] || null
|
|
288
|
-
}
|
|
288
|
+
};
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
supportedKeys() {
|
|
292
|
-
return ['hyperPodCluster']
|
|
292
|
+
return ['hyperPodCluster'];
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
295
|
|
|
@@ -297,7 +297,7 @@ class ClusterResolver extends DynamicResolver {
|
|
|
297
297
|
const server = new McpServer({
|
|
298
298
|
name: 'hyperpod-cluster-picker',
|
|
299
299
|
version: '1.0.0'
|
|
300
|
-
})
|
|
300
|
+
});
|
|
301
301
|
|
|
302
302
|
// Register the get_hyperpod_clusters tool
|
|
303
303
|
server.tool(
|
|
@@ -316,57 +316,57 @@ server.tool(
|
|
|
316
316
|
type: 'text',
|
|
317
317
|
text: JSON.stringify({ values: {}, choices: {} })
|
|
318
318
|
}]
|
|
319
|
-
}
|
|
319
|
+
};
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
const region = context?.awsRegion || process.env.AWS_REGION || 'us-east-1'
|
|
323
|
-
const profile = context?.awsProfile || process.env.AWS_PROFILE || null
|
|
324
|
-
log(`Querying HyperPod clusters in region: ${region}${profile ? ` (profile: ${profile})` : ''}`)
|
|
322
|
+
const region = context?.awsRegion || process.env.AWS_REGION || 'us-east-1';
|
|
323
|
+
const profile = context?.awsProfile || process.env.AWS_PROFILE || null;
|
|
324
|
+
log(`Querying HyperPod clusters in region: ${region}${profile ? ` (profile: ${profile})` : ''}`);
|
|
325
325
|
|
|
326
326
|
try {
|
|
327
|
-
await _ensureSdkLoaded()
|
|
327
|
+
await _ensureSdkLoaded();
|
|
328
328
|
|
|
329
|
-
let clusters = null
|
|
330
|
-
let lastError = null
|
|
329
|
+
let clusters = null;
|
|
330
|
+
let lastError = null;
|
|
331
331
|
|
|
332
332
|
// Strategy 1: If a specific profile was requested, use it directly
|
|
333
333
|
if (profile) {
|
|
334
334
|
try {
|
|
335
|
-
log(`Trying explicit profile: ${profile}`)
|
|
336
|
-
const client = _createClientWithProfile(region, profile)
|
|
337
|
-
clusters = await fetchHyperPodClusters(client, { limit })
|
|
335
|
+
log(`Trying explicit profile: ${profile}`);
|
|
336
|
+
const client = _createClientWithProfile(region, profile);
|
|
337
|
+
clusters = await fetchHyperPodClusters(client, { limit });
|
|
338
338
|
} catch (err) {
|
|
339
|
-
log(`Profile "${profile}" failed: ${err.message}`)
|
|
340
|
-
lastError = err
|
|
339
|
+
log(`Profile "${profile}" failed: ${err.message}`);
|
|
340
|
+
lastError = err;
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
// Strategy 2: Try the default credential chain (env vars, instance profile, etc.)
|
|
345
345
|
if (!clusters) {
|
|
346
346
|
try {
|
|
347
|
-
log('Trying default credential chain')
|
|
348
|
-
const client = createSageMakerClient(region)
|
|
349
|
-
clusters = await fetchHyperPodClusters(client, { limit })
|
|
347
|
+
log('Trying default credential chain');
|
|
348
|
+
const client = createSageMakerClient(region);
|
|
349
|
+
clusters = await fetchHyperPodClusters(client, { limit });
|
|
350
350
|
} catch (err) {
|
|
351
|
-
log(`Default credential chain failed: ${err.message}`)
|
|
352
|
-
lastError = err
|
|
351
|
+
log(`Default credential chain failed: ${err.message}`);
|
|
352
|
+
lastError = err;
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
// Strategy 3: Detect available AWS profiles and try each
|
|
357
357
|
if (!clusters && _fromIni) {
|
|
358
|
-
const profiles = _detectAwsProfiles()
|
|
358
|
+
const profiles = _detectAwsProfiles();
|
|
359
359
|
if (profiles.length > 0) {
|
|
360
|
-
log(`Default credentials failed, trying ${profiles.length} detected profile(s): ${profiles.join(', ')}`)
|
|
360
|
+
log(`Default credentials failed, trying ${profiles.length} detected profile(s): ${profiles.join(', ')}`);
|
|
361
361
|
for (const p of profiles) {
|
|
362
362
|
try {
|
|
363
|
-
const client = _createClientWithProfile(region, p)
|
|
364
|
-
clusters = await fetchHyperPodClusters(client, { limit })
|
|
365
|
-
log(`Profile "${p}" succeeded`)
|
|
366
|
-
break
|
|
363
|
+
const client = _createClientWithProfile(region, p);
|
|
364
|
+
clusters = await fetchHyperPodClusters(client, { limit });
|
|
365
|
+
log(`Profile "${p}" succeeded`);
|
|
366
|
+
break;
|
|
367
367
|
} catch (err) {
|
|
368
|
-
log(`Profile "${p}" failed: ${err.message}`)
|
|
369
|
-
lastError = err
|
|
368
|
+
log(`Profile "${p}" failed: ${err.message}`);
|
|
369
|
+
lastError = err;
|
|
370
370
|
}
|
|
371
371
|
}
|
|
372
372
|
}
|
|
@@ -374,15 +374,15 @@ server.tool(
|
|
|
374
374
|
|
|
375
375
|
// If all strategies failed, throw the last error
|
|
376
376
|
if (!clusters) {
|
|
377
|
-
throw lastError || new Error('No AWS credentials available')
|
|
377
|
+
throw lastError || new Error('No AWS credentials available');
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
-
const result = buildResponse(clusters)
|
|
380
|
+
const result = buildResponse(clusters);
|
|
381
381
|
|
|
382
382
|
if (clusters.length > 0) {
|
|
383
|
-
log(`Found ${clusters.length} HyperPod EKS cluster(s)`)
|
|
383
|
+
log(`Found ${clusters.length} HyperPod EKS cluster(s)`);
|
|
384
384
|
} else {
|
|
385
|
-
log('No InService HyperPod EKS clusters found')
|
|
385
|
+
log('No InService HyperPod EKS clusters found');
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
return {
|
|
@@ -390,35 +390,35 @@ server.tool(
|
|
|
390
390
|
type: 'text',
|
|
391
391
|
text: JSON.stringify(result)
|
|
392
392
|
}]
|
|
393
|
-
}
|
|
393
|
+
};
|
|
394
394
|
} catch (err) {
|
|
395
|
-
log(`Error querying clusters: ${err.message}`)
|
|
395
|
+
log(`Error querying clusters: ${err.message}`);
|
|
396
396
|
const errorResult = {
|
|
397
397
|
values: {},
|
|
398
398
|
choices: { hyperPodCluster: [] },
|
|
399
399
|
error: err.message,
|
|
400
400
|
message: `Failed to query HyperPod clusters: ${err.message}`
|
|
401
|
-
}
|
|
401
|
+
};
|
|
402
402
|
return {
|
|
403
403
|
content: [{
|
|
404
404
|
type: 'text',
|
|
405
405
|
text: JSON.stringify(errorResult)
|
|
406
406
|
}]
|
|
407
|
-
}
|
|
407
|
+
};
|
|
408
408
|
}
|
|
409
409
|
}
|
|
410
|
-
)
|
|
410
|
+
);
|
|
411
411
|
|
|
412
412
|
// Export for testing
|
|
413
|
-
export { fetchHyperPodClusters, buildResponse, createSageMakerClient, _ensureSdkLoaded, ClusterResolver }
|
|
413
|
+
export { fetchHyperPodClusters, buildResponse, createSageMakerClient, _ensureSdkLoaded, ClusterResolver };
|
|
414
414
|
|
|
415
415
|
// Guard MCP transport — only connect when run as main module
|
|
416
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
417
|
-
const isMain = process.argv[1] && resolve(process.argv[1]) === __filename
|
|
416
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
417
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === __filename;
|
|
418
418
|
|
|
419
419
|
if (isMain) {
|
|
420
|
-
log('Starting HyperPod Cluster Picker MCP server')
|
|
421
|
-
await _ensureSdkLoaded()
|
|
422
|
-
const transport = new StdioServerTransport()
|
|
423
|
-
await server.connect(transport)
|
|
420
|
+
log('Starting HyperPod Cluster Picker MCP server');
|
|
421
|
+
await _ensureSdkLoaded();
|
|
422
|
+
const transport = new StdioServerTransport();
|
|
423
|
+
await server.connect(transport);
|
|
424
424
|
}
|