@aws/ml-container-creator 0.10.0 → 0.12.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.
Files changed (90) hide show
  1. package/LICENSE-THIRD-PARTY +9304 -0
  2. package/bin/cli.js +2 -0
  3. package/config/bootstrap-e2e-stack.json +341 -0
  4. package/config/bootstrap-stack.json +40 -3
  5. package/config/parameter-schema-v2.json +33 -22
  6. package/config/tune-catalog.json +1781 -0
  7. package/infra/ci-harness/buildspec.yml +1 -0
  8. package/infra/ci-harness/lambda/path-prover/brain.ts +306 -0
  9. package/infra/ci-harness/lambda/path-prover/write-results.ts +152 -0
  10. package/infra/ci-harness/lib/ci-harness-stack.ts +851 -7
  11. package/infra/ci-harness/state-machines/path-prover.asl.json +496 -0
  12. package/package.json +53 -67
  13. package/servers/base-image-picker/index.js +121 -121
  14. package/servers/e2e-status/index.js +297 -0
  15. package/servers/e2e-status/manifest.json +14 -0
  16. package/servers/e2e-status/package.json +15 -0
  17. package/servers/endpoint-picker/LICENSE +202 -0
  18. package/servers/endpoint-picker/index.js +536 -0
  19. package/servers/endpoint-picker/manifest.json +14 -0
  20. package/servers/endpoint-picker/package.json +18 -0
  21. package/servers/hyperpod-cluster-picker/index.js +125 -125
  22. package/servers/instance-sizer/index.js +166 -153
  23. package/servers/instance-sizer/lib/instance-ranker.js +120 -76
  24. package/servers/instance-sizer/lib/model-resolver.js +61 -61
  25. package/servers/instance-sizer/lib/quota-resolver.js +113 -113
  26. package/servers/instance-sizer/lib/vram-estimator.js +31 -31
  27. package/servers/lib/bedrock-client.js +38 -38
  28. package/servers/lib/catalogs/instances.json +27 -0
  29. package/servers/lib/catalogs/model-servers.json +201 -3
  30. package/servers/lib/custom-validators.js +13 -13
  31. package/servers/lib/dynamic-resolver.js +4 -4
  32. package/servers/marketplace-picker/index.js +342 -0
  33. package/servers/marketplace-picker/manifest.json +14 -0
  34. package/servers/marketplace-picker/package.json +18 -0
  35. package/servers/model-picker/index.js +382 -382
  36. package/servers/region-picker/index.js +56 -56
  37. package/servers/workload-picker/LICENSE +202 -0
  38. package/servers/workload-picker/catalogs/workload-profiles.json +67 -0
  39. package/servers/workload-picker/index.js +171 -0
  40. package/servers/workload-picker/manifest.json +16 -0
  41. package/servers/workload-picker/package.json +16 -0
  42. package/src/app.js +12 -3
  43. package/src/lib/bootstrap-command-handler.js +609 -15
  44. package/src/lib/bootstrap-config.js +36 -0
  45. package/src/lib/bootstrap-profile-manager.js +48 -41
  46. package/src/lib/ci-register-helpers.js +74 -0
  47. package/src/lib/config-loader.js +3 -0
  48. package/src/lib/config-manager.js +7 -0
  49. package/src/lib/config-validator.js +1 -1
  50. package/src/lib/cuda-resolver.js +17 -8
  51. package/src/lib/generated/cli-options.js +319 -314
  52. package/src/lib/generated/parameter-matrix.js +672 -661
  53. package/src/lib/generated/validation-rules.js +76 -72
  54. package/src/lib/path-prover-brain.js +664 -0
  55. package/src/lib/prompts/infrastructure-prompts.js +2 -2
  56. package/src/lib/prompts/model-prompts.js +6 -0
  57. package/src/lib/prompts/project-prompts.js +12 -0
  58. package/src/lib/secrets-prompt-runner.js +4 -0
  59. package/src/lib/template-manager.js +1 -1
  60. package/src/lib/template-variable-resolver.js +87 -1
  61. package/src/lib/tune-catalog-validator.js +37 -4
  62. package/templates/Dockerfile +9 -0
  63. package/templates/code/adapter_sidecar.py +444 -0
  64. package/templates/code/serve +6 -0
  65. package/templates/code/serve.d/vllm.ejs +1 -1
  66. package/templates/do/.benchmark_writer.py +1476 -0
  67. package/templates/do/.tune_helper.py +982 -57
  68. package/templates/do/__pycache__/.benchmark_writer.cpython-312.pyc +0 -0
  69. package/templates/do/adapter +154 -0
  70. package/templates/do/benchmark +639 -85
  71. package/templates/do/build +5 -0
  72. package/templates/do/clean.d/async-inference.ejs +5 -0
  73. package/templates/do/clean.d/batch-transform.ejs +5 -0
  74. package/templates/do/clean.d/hyperpod-eks.ejs +5 -0
  75. package/templates/do/clean.d/managed-inference.ejs +5 -0
  76. package/templates/do/config +115 -45
  77. package/templates/do/deploy.d/async-inference.ejs +30 -3
  78. package/templates/do/deploy.d/batch-transform.ejs +29 -3
  79. package/templates/do/deploy.d/hyperpod-eks.ejs +4 -0
  80. package/templates/do/deploy.d/managed-inference.ejs +216 -14
  81. package/templates/do/lib/endpoint-config.sh +1 -1
  82. package/templates/do/lib/profile.sh +44 -0
  83. package/templates/do/optimize +106 -37
  84. package/templates/do/push +5 -0
  85. package/templates/do/register +94 -0
  86. package/templates/do/stage +567 -0
  87. package/templates/do/submit +7 -0
  88. package/templates/do/test +14 -0
  89. package/templates/do/tune +382 -59
  90. package/templates/do/validate +44 -4
@@ -18,18 +18,18 @@
18
18
  * Returns: { values, choices, metadata }
19
19
  */
20
20
 
21
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
22
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
23
- import { z } from 'zod'
24
- import { readFileSync } from 'node:fs'
25
- import { fileURLToPath } from 'node:url'
26
- import { resolve, dirname } from 'node:path'
27
- import { DynamicResolver as DynamicResolverBase } from '../lib/dynamic-resolver.js'
21
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
22
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
23
+ import { z } from 'zod';
24
+ import { readFileSync } from 'node:fs';
25
+ import { fileURLToPath } from 'node:url';
26
+ import { resolve, dirname } from 'node:path';
27
+ import { DynamicResolver as DynamicResolverBase } from '../lib/dynamic-resolver.js';
28
28
 
29
29
  // ── Catalog loader ───────────────────────────────────────────────────────────
30
30
 
31
- const __filename = fileURLToPath(import.meta.url)
32
- const __dirname = dirname(__filename)
31
+ const __filename = fileURLToPath(import.meta.url);
32
+ const __dirname = dirname(__filename);
33
33
 
34
34
  /**
35
35
  * Load and parse a JSON catalog file relative to the server directory.
@@ -39,17 +39,17 @@ const __dirname = dirname(__filename)
39
39
  * @returns {any} Parsed JSON content
40
40
  */
41
41
  function loadCatalog(relativePath) {
42
- const fullPath = resolve(__dirname, relativePath)
43
- let raw
42
+ const fullPath = resolve(__dirname, relativePath);
43
+ let raw;
44
44
  try {
45
- raw = readFileSync(fullPath, 'utf8')
45
+ raw = readFileSync(fullPath, 'utf8');
46
46
  } catch (err) {
47
- throw new Error(`Catalog file not found: ${fullPath}`)
47
+ throw new Error(`Catalog file not found: ${fullPath}`);
48
48
  }
49
49
  try {
50
- return JSON.parse(raw)
50
+ return JSON.parse(raw);
51
51
  } catch (err) {
52
- throw new Error(`Failed to parse catalog ${fullPath}: ${err.message}`)
52
+ throw new Error(`Failed to parse catalog ${fullPath}: ${err.message}`);
53
53
  }
54
54
  }
55
55
 
@@ -74,8 +74,8 @@ class ImageResolver extends DynamicResolverBase {
74
74
  * @param {string} [options.searchCriteria] - Optional filter string
75
75
  * @returns {Promise<{images: object[], defaultImage: string|null}>}
76
76
  */
77
- async fetchImages(framework, options = {}) {
78
- throw new Error('fetchImages() must be implemented by subclass')
77
+ async fetchImages(framework, _options = {}) {
78
+ throw new Error('fetchImages() must be implemented by subclass');
79
79
  }
80
80
 
81
81
  /**
@@ -83,33 +83,33 @@ class ImageResolver extends DynamicResolverBase {
83
83
  * @returns {string[]}
84
84
  */
85
85
  supportedFrameworks() {
86
- throw new Error('supportedFrameworks() must be implemented by subclass')
86
+ throw new Error('supportedFrameworks() must be implemented by subclass');
87
87
  }
88
88
 
89
89
  // ── DynamicResolver interface bridge ─────────────────────────────────
90
90
 
91
91
  async fetch(key, options = {}) {
92
- return this.fetchImages(key, options)
92
+ return this.fetchImages(key, options);
93
93
  }
94
94
 
95
95
  supportedKeys() {
96
- return this.supportedFrameworks()
96
+ return this.supportedFrameworks();
97
97
  }
98
98
  }
99
99
 
100
100
  // ── Load catalogs from JSON files ─────────────────────────────────────────────
101
101
 
102
- let TRANSFORMER_IMAGE_CATALOG
103
- let PYTHON_SLIM_CATALOG
104
- let TRITON_IMAGE_CATALOG
102
+ let TRANSFORMER_IMAGE_CATALOG;
103
+ let PYTHON_SLIM_CATALOG;
104
+ let TRITON_IMAGE_CATALOG;
105
105
 
106
106
  try {
107
- TRANSFORMER_IMAGE_CATALOG = loadCatalog('../lib/catalogs/model-servers.json')
108
- PYTHON_SLIM_CATALOG = loadCatalog('../lib/catalogs/python-slim.json')
109
- TRITON_IMAGE_CATALOG = loadCatalog('../lib/catalogs/triton.json')
107
+ TRANSFORMER_IMAGE_CATALOG = loadCatalog('../lib/catalogs/model-servers.json');
108
+ PYTHON_SLIM_CATALOG = loadCatalog('../lib/catalogs/python-slim.json');
109
+ TRITON_IMAGE_CATALOG = loadCatalog('../lib/catalogs/triton.json');
110
110
  } catch (err) {
111
- process.stderr.write(`[base-image-picker] Fatal: ${err.message}\n`)
112
- process.exit(1)
111
+ process.stderr.write(`[base-image-picker] Fatal: ${err.message}\n`);
112
+ process.exit(1);
113
113
  }
114
114
 
115
115
  // ── DynamicResolver ──────────────────────────────────────────────────────────
@@ -123,39 +123,39 @@ try {
123
123
  */
124
124
  class DynamicResolver extends ImageResolver {
125
125
  constructor(options = {}) {
126
- super()
127
- this._timeout = options.timeout || 5000
126
+ super();
127
+ this._timeout = options.timeout || 5000;
128
128
  // Registry API endpoints per framework
129
129
  this._registryEndpoints = {
130
130
  'vllm': 'https://hub.docker.com/v2/repositories/vllm/vllm-openai/tags',
131
131
  'sglang': 'https://hub.docker.com/v2/repositories/lmsysorg/sglang/tags',
132
132
  'djl': 'https://hub.docker.com/v2/repositories/deepjavalibrary/djl-serving/tags'
133
133
  // tensorrt-llm and lmi require auth — not supported in V1 discover
134
- }
134
+ };
135
135
  }
136
136
 
137
137
  async fetchImages(framework, options = {}) {
138
- const { limit = 5 } = options
139
- const endpoint = this._registryEndpoints[framework]
138
+ const { limit = 5 } = options;
139
+ const endpoint = this._registryEndpoints[framework];
140
140
  if (!endpoint) {
141
- return { images: [], defaultImage: null }
141
+ return { images: [], defaultImage: null };
142
142
  }
143
143
 
144
- const controller = new AbortController()
145
- const timer = setTimeout(() => controller.abort(), this._timeout)
144
+ const controller = new AbortController();
145
+ const timer = setTimeout(() => controller.abort(), this._timeout);
146
146
 
147
147
  try {
148
148
  const response = await fetch(
149
149
  `${endpoint}?page_size=${limit}&ordering=-last_updated`,
150
150
  { signal: controller.signal }
151
- )
152
- clearTimeout(timer)
151
+ );
152
+ clearTimeout(timer);
153
153
 
154
154
  if (!response.ok) {
155
- throw new Error(`Registry API returned ${response.status}`)
155
+ throw new Error(`Registry API returned ${response.status}`);
156
156
  }
157
157
 
158
- const data = await response.json()
158
+ const data = await response.json();
159
159
  const images = (data.results || []).map(tag => ({
160
160
  image: `${this._repoForFramework(framework)}:${tag.name}`,
161
161
  tag: tag.name,
@@ -164,22 +164,22 @@ class DynamicResolver extends ImageResolver {
164
164
  labels: {},
165
165
  registry: 'dockerhub',
166
166
  repository: this._repoForFramework(framework)
167
- }))
167
+ }));
168
168
 
169
169
  return {
170
170
  images: images.slice(0, limit),
171
171
  defaultImage: images[0]?.image || null
172
- }
172
+ };
173
173
  } catch (err) {
174
- log(`[discover] Registry API failed for ${framework}: ${err.message}`)
175
- return { images: [], defaultImage: null }
174
+ log(`[discover] Registry API failed for ${framework}: ${err.message}`);
175
+ return { images: [], defaultImage: null };
176
176
  } finally {
177
- clearTimeout(timer)
177
+ clearTimeout(timer);
178
178
  }
179
179
  }
180
180
 
181
181
  supportedFrameworks() {
182
- return Object.keys(this._registryEndpoints)
182
+ return Object.keys(this._registryEndpoints);
183
183
  }
184
184
 
185
185
  _repoForFramework(framework) {
@@ -187,8 +187,8 @@ class DynamicResolver extends ImageResolver {
187
187
  'vllm': 'vllm/vllm-openai',
188
188
  'sglang': 'lmsysorg/sglang',
189
189
  'djl': 'deepjavalibrary/djl-serving'
190
- }
191
- return map[framework] || framework
190
+ };
191
+ return map[framework] || framework;
192
192
  }
193
193
  }
194
194
 
@@ -209,14 +209,14 @@ class DynamicResolver extends ImageResolver {
209
209
  * @returns {object[]} Merged, deduplicated image list
210
210
  */
211
211
  function mergeStaticAndDynamic(staticImages, dynamicImages, limit) {
212
- const staticIds = new Set(staticImages.map(e => e.image))
213
- const netNew = dynamicImages.filter(e => !staticIds.has(e.image))
212
+ const staticIds = new Set(staticImages.map(e => e.image));
213
+ const netNew = dynamicImages.filter(e => !staticIds.has(e.image));
214
214
 
215
215
  // Sort net-new by created desc
216
- netNew.sort((a, b) => new Date(b.created) - new Date(a.created))
216
+ netNew.sort((a, b) => new Date(b.created) - new Date(a.created));
217
217
 
218
- const merged = [...staticImages, ...netNew]
219
- return limit != null ? merged.slice(0, limit) : merged
218
+ const merged = [...staticImages, ...netNew];
219
+ return limit !== null && limit !== undefined ? merged.slice(0, limit) : merged;
220
220
  }
221
221
 
222
222
  // ── StaticCatalogResolver ────────────────────────────────────────────────────
@@ -229,29 +229,29 @@ function mergeStaticAndDynamic(staticImages, dynamicImages, limit) {
229
229
  */
230
230
  class StaticCatalogResolver extends ImageResolver {
231
231
  constructor(transformerCatalog, pythonSlimCatalog, tritonCatalog) {
232
- super()
233
- this._transformerCatalog = transformerCatalog
234
- this._pythonSlimCatalog = pythonSlimCatalog
235
- this._tritonCatalog = tritonCatalog || []
232
+ super();
233
+ this._transformerCatalog = transformerCatalog;
234
+ this._pythonSlimCatalog = pythonSlimCatalog;
235
+ this._tritonCatalog = tritonCatalog || [];
236
236
  }
237
237
 
238
238
  async fetchImages(framework, options = {}) {
239
- const { limit = 5, searchCriteria } = options
239
+ const { limit = 5, searchCriteria } = options;
240
240
 
241
241
  if (framework === 'python-slim') {
242
- return this._resolvePythonSlim(limit, searchCriteria)
242
+ return this._resolvePythonSlim(limit, searchCriteria);
243
243
  }
244
244
 
245
245
  if (framework === 'triton') {
246
- return this._resolveTriton(limit, searchCriteria)
246
+ return this._resolveTriton(limit, searchCriteria);
247
247
  }
248
248
 
249
- const catalog = this._transformerCatalog[framework] || []
250
- const sliced = catalog.slice(0, limit)
249
+ const catalog = this._transformerCatalog[framework] || [];
250
+ const sliced = catalog.slice(0, limit);
251
251
  return {
252
252
  images: sliced,
253
253
  defaultImage: sliced[0]?.image || null
254
- }
254
+ };
255
255
  }
256
256
 
257
257
  supportedFrameworks() {
@@ -259,46 +259,46 @@ class StaticCatalogResolver extends ImageResolver {
259
259
  ...Object.keys(this._transformerCatalog),
260
260
  'python-slim',
261
261
  'triton'
262
- ]
262
+ ];
263
263
  }
264
264
 
265
265
  _resolvePythonSlim(limit, searchCriteria) {
266
- let catalog = [...this._pythonSlimCatalog]
266
+ let catalog = [...this._pythonSlimCatalog];
267
267
 
268
268
  if (searchCriteria && searchCriteria.trim()) {
269
- const query = searchCriteria.trim().toLowerCase()
269
+ const query = searchCriteria.trim().toLowerCase();
270
270
  catalog = catalog.filter(entry =>
271
271
  entry.tag.toLowerCase().includes(query) ||
272
272
  entry.image.toLowerCase().includes(query) ||
273
273
  (entry.labels.python_version && entry.labels.python_version.toLowerCase().includes(query))
274
- )
274
+ );
275
275
  }
276
276
 
277
- const sliced = catalog.slice(0, limit)
277
+ const sliced = catalog.slice(0, limit);
278
278
  return {
279
279
  images: sliced,
280
280
  defaultImage: sliced[0]?.image || null
281
- }
281
+ };
282
282
  }
283
283
 
284
284
  _resolveTriton(limit, searchCriteria) {
285
- let catalog = [...this._tritonCatalog]
285
+ let catalog = [...this._tritonCatalog];
286
286
 
287
287
  if (searchCriteria && searchCriteria.trim()) {
288
- const query = searchCriteria.trim().toLowerCase()
288
+ const query = searchCriteria.trim().toLowerCase();
289
289
  catalog = catalog.filter(entry =>
290
290
  entry.tag.toLowerCase().includes(query) ||
291
291
  entry.image.toLowerCase().includes(query) ||
292
292
  (entry.labels.triton_version && entry.labels.triton_version.toLowerCase().includes(query)) ||
293
293
  (entry.labels.cuda_version && entry.labels.cuda_version.toLowerCase().includes(query))
294
- )
294
+ );
295
295
  }
296
296
 
297
- const sliced = catalog.slice(0, limit)
297
+ const sliced = catalog.slice(0, limit);
298
298
  return {
299
299
  images: sliced,
300
300
  defaultImage: sliced[0]?.image || null
301
- }
301
+ };
302
302
  }
303
303
  }
304
304
 
@@ -312,8 +312,8 @@ class StaticCatalogResolver extends ImageResolver {
312
312
  */
313
313
  class ResolverRegistry {
314
314
  constructor() {
315
- this._resolvers = new Map()
316
- this._defaultResolver = null
315
+ this._resolvers = new Map();
316
+ this._defaultResolver = null;
317
317
  }
318
318
 
319
319
  /**
@@ -322,7 +322,7 @@ class ResolverRegistry {
322
322
  */
323
323
  register(resolver) {
324
324
  for (const framework of resolver.supportedFrameworks()) {
325
- this._resolvers.set(framework, resolver)
325
+ this._resolvers.set(framework, resolver);
326
326
  }
327
327
  }
328
328
 
@@ -331,7 +331,7 @@ class ResolverRegistry {
331
331
  * @param {ImageResolver} resolver
332
332
  */
333
333
  setDefault(resolver) {
334
- this._defaultResolver = resolver
334
+ this._defaultResolver = resolver;
335
335
  }
336
336
 
337
337
  /**
@@ -340,16 +340,16 @@ class ResolverRegistry {
340
340
  * @returns {ImageResolver|null}
341
341
  */
342
342
  getResolver(framework) {
343
- return this._resolvers.get(framework) || this._defaultResolver
343
+ return this._resolvers.get(framework) || this._defaultResolver;
344
344
  }
345
345
  }
346
346
 
347
347
  // ── V1 wiring ────────────────────────────────────────────────────────────────
348
348
 
349
- const staticResolver = new StaticCatalogResolver(TRANSFORMER_IMAGE_CATALOG, PYTHON_SLIM_CATALOG, TRITON_IMAGE_CATALOG)
350
- const registry = new ResolverRegistry()
351
- registry.register(staticResolver)
352
- registry.setDefault(staticResolver)
349
+ const staticResolver = new StaticCatalogResolver(TRANSFORMER_IMAGE_CATALOG, PYTHON_SLIM_CATALOG, TRITON_IMAGE_CATALOG);
350
+ const registry = new ResolverRegistry();
351
+ registry.register(staticResolver);
352
+ registry.setDefault(staticResolver);
353
353
 
354
354
  // ── Discover mode ────────────────────────────────────────────────────────────
355
355
 
@@ -358,13 +358,13 @@ registry.setDefault(staticResolver)
358
358
  * --discover flag or MCP_DISCOVER=true activates discover mode.
359
359
  */
360
360
  const discoverMode = process.argv.includes('--discover') ||
361
- process.env.MCP_DISCOVER === 'true'
361
+ process.env.MCP_DISCOVER === 'true';
362
362
 
363
- let dynamicResolver = null
363
+ let dynamicResolver = null;
364
364
 
365
365
  if (discoverMode) {
366
- dynamicResolver = new DynamicResolver()
367
- registry.register(dynamicResolver)
366
+ dynamicResolver = new DynamicResolver();
367
+ registry.register(dynamicResolver);
368
368
  }
369
369
 
370
370
  // ── Routing logic ────────────────────────────────────────────────────────────
@@ -375,52 +375,52 @@ if (discoverMode) {
375
375
  * When discover mode is active, merges static and dynamic results.
376
376
  */
377
377
  async function resolveBaseImage(context, limit) {
378
- const { framework, modelServer, searchCriteria, architecture } = context
378
+ const { framework, modelServer, searchCriteria, architecture } = context;
379
379
 
380
380
  // Determine which framework identifier to resolve
381
- let resolverKey
381
+ let resolverKey;
382
382
  if (architecture === 'triton') {
383
- resolverKey = 'triton'
383
+ resolverKey = 'triton';
384
384
  } else if (architecture === 'diffusors' && modelServer) {
385
- resolverKey = modelServer
385
+ resolverKey = modelServer;
386
386
  } else if (framework === 'transformers' && modelServer) {
387
- resolverKey = modelServer
387
+ resolverKey = modelServer;
388
388
  } else {
389
- resolverKey = 'python-slim'
389
+ resolverKey = 'python-slim';
390
390
  }
391
391
 
392
- const resolver = registry.getResolver(resolverKey)
392
+ const resolver = registry.getResolver(resolverKey);
393
393
  if (!resolver) {
394
- return { values: { baseImage: null }, choices: { baseImage: [] }, metadata: { baseImage: [] } }
394
+ return { values: { baseImage: null }, choices: { baseImage: [] }, metadata: { baseImage: [] } };
395
395
  }
396
396
 
397
- let resultImages
397
+ let resultImages;
398
398
 
399
399
  if (discoverMode && dynamicResolver && dynamicResolver.supportedFrameworks().includes(resolverKey)) {
400
400
  // Fetch both static and dynamic results, then merge
401
- const staticResult = await staticResolver.fetchImages(resolverKey, { limit, searchCriteria })
402
- const dynamicResult = await dynamicResolver.fetchImages(resolverKey, { limit: 5 })
401
+ const staticResult = await staticResolver.fetchImages(resolverKey, { limit, searchCriteria });
402
+ const dynamicResult = await dynamicResolver.fetchImages(resolverKey, { limit: 5 });
403
403
 
404
- resultImages = mergeStaticAndDynamic(staticResult.images, dynamicResult.images, limit)
404
+ resultImages = mergeStaticAndDynamic(staticResult.images, dynamicResult.images, limit);
405
405
  } else {
406
406
  // Static-only path (no network calls)
407
- const result = await resolver.fetchImages(resolverKey, { limit, searchCriteria })
408
- resultImages = result.images
407
+ const result = await resolver.fetchImages(resolverKey, { limit, searchCriteria });
408
+ resultImages = result.images;
409
409
  }
410
410
 
411
- const images = resultImages.map(e => e.image)
411
+ const images = resultImages.map(e => e.image);
412
412
  return {
413
413
  values: { baseImage: images[0] || null },
414
414
  choices: { baseImage: images },
415
415
  metadata: { baseImage: resultImages }
416
- }
416
+ };
417
417
  }
418
418
 
419
419
  /**
420
420
  * Log to stderr so it doesn't interfere with MCP stdio protocol on stdout.
421
421
  */
422
422
  function log(message) {
423
- process.stderr.write(`[base-image-picker] ${message}\n`)
423
+ process.stderr.write(`[base-image-picker] ${message}\n`);
424
424
  }
425
425
 
426
426
  // ── MCP Server ───────────────────────────────────────────────────────────────
@@ -428,7 +428,7 @@ function log(message) {
428
428
  const server = new McpServer({
429
429
  name: 'base-image-picker',
430
430
  version: '1.0.0'
431
- })
431
+ });
432
432
 
433
433
  server.tool(
434
434
  'get_base_images',
@@ -439,15 +439,15 @@ server.tool(
439
439
  context: z.record(z.string(), z.any()).optional().describe('Current configuration context (framework, modelServer, searchCriteria)')
440
440
  },
441
441
  async ({ parameters, limit, context }) => {
442
- const values = {}
443
- const choices = {}
444
- const metadata = {}
442
+ const values = {};
443
+ const choices = {};
444
+ const metadata = {};
445
445
 
446
446
  if (parameters.includes('baseImage')) {
447
- const result = await resolveBaseImage(context || {}, limit)
448
- Object.assign(values, result.values)
449
- Object.assign(choices, result.choices)
450
- Object.assign(metadata, result.metadata)
447
+ const result = await resolveBaseImage(context || {}, limit);
448
+ Object.assign(values, result.values);
449
+ Object.assign(choices, result.choices);
450
+ Object.assign(metadata, result.metadata);
451
451
  }
452
452
 
453
453
  return {
@@ -455,9 +455,9 @@ server.tool(
455
455
  type: 'text',
456
456
  text: JSON.stringify({ values, choices, metadata })
457
457
  }]
458
- }
458
+ };
459
459
  }
460
- )
460
+ );
461
461
 
462
462
  // ── Exports for testing ──────────────────────────────────────────────────────
463
463
 
@@ -476,20 +476,20 @@ export {
476
476
  staticResolver,
477
477
  dynamicResolver,
478
478
  discoverMode
479
- }
479
+ };
480
480
 
481
- export { DynamicResolverBase as DynamicResolverBase }
481
+ export { DynamicResolverBase as DynamicResolverBase };
482
482
 
483
483
  // ── Main guard ───────────────────────────────────────────────────────────────
484
484
 
485
- const isMain = process.argv[1] && resolve(process.argv[1]) === __filename
485
+ const isMain = process.argv[1] && resolve(process.argv[1]) === __filename;
486
486
 
487
487
  if (isMain) {
488
488
  if (discoverMode) {
489
- log('Discover mode — serving curated catalogs + live registry lookups')
489
+ log('Discover mode — serving curated catalogs + live registry lookups');
490
490
  } else {
491
- log('Static mode — serving curated base image catalogs')
491
+ log('Static mode — serving curated base image catalogs');
492
492
  }
493
- const transport = new StdioServerTransport()
494
- await server.connect(transport)
493
+ const transport = new StdioServerTransport();
494
+ await server.connect(transport);
495
495
  }