@aws/ml-container-creator 0.2.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.
Files changed (143) hide show
  1. package/LICENSE +202 -0
  2. package/LICENSE-THIRD-PARTY +68620 -0
  3. package/NOTICE +2 -0
  4. package/README.md +106 -0
  5. package/bin/cli.js +365 -0
  6. package/config/defaults.json +32 -0
  7. package/config/presets/transformers-djl.json +26 -0
  8. package/config/presets/transformers-gpu.json +24 -0
  9. package/config/presets/transformers-lmi.json +27 -0
  10. package/package.json +129 -0
  11. package/servers/README.md +419 -0
  12. package/servers/base-image-picker/catalogs/model-servers.json +1191 -0
  13. package/servers/base-image-picker/catalogs/python-slim.json +38 -0
  14. package/servers/base-image-picker/catalogs/triton-backends.json +51 -0
  15. package/servers/base-image-picker/catalogs/triton.json +38 -0
  16. package/servers/base-image-picker/index.js +495 -0
  17. package/servers/base-image-picker/manifest.json +17 -0
  18. package/servers/base-image-picker/package.json +15 -0
  19. package/servers/hyperpod-cluster-picker/LICENSE +202 -0
  20. package/servers/hyperpod-cluster-picker/index.js +424 -0
  21. package/servers/hyperpod-cluster-picker/manifest.json +14 -0
  22. package/servers/hyperpod-cluster-picker/package.json +17 -0
  23. package/servers/instance-recommender/LICENSE +202 -0
  24. package/servers/instance-recommender/catalogs/instances.json +852 -0
  25. package/servers/instance-recommender/index.js +284 -0
  26. package/servers/instance-recommender/manifest.json +16 -0
  27. package/servers/instance-recommender/package.json +15 -0
  28. package/servers/lib/LICENSE +202 -0
  29. package/servers/lib/bedrock-client.js +160 -0
  30. package/servers/lib/custom-validators.js +46 -0
  31. package/servers/lib/dynamic-resolver.js +36 -0
  32. package/servers/lib/package.json +11 -0
  33. package/servers/lib/schemas/image-catalog.schema.json +185 -0
  34. package/servers/lib/schemas/instances.schema.json +124 -0
  35. package/servers/lib/schemas/manifest.schema.json +64 -0
  36. package/servers/lib/schemas/model-catalog.schema.json +91 -0
  37. package/servers/lib/schemas/regions.schema.json +26 -0
  38. package/servers/lib/schemas/triton-backends.schema.json +51 -0
  39. package/servers/model-picker/catalogs/jumpstart-public.json +66 -0
  40. package/servers/model-picker/catalogs/popular-diffusors.json +88 -0
  41. package/servers/model-picker/catalogs/popular-transformers.json +226 -0
  42. package/servers/model-picker/index.js +1693 -0
  43. package/servers/model-picker/manifest.json +18 -0
  44. package/servers/model-picker/package.json +20 -0
  45. package/servers/region-picker/LICENSE +202 -0
  46. package/servers/region-picker/catalogs/regions.json +263 -0
  47. package/servers/region-picker/index.js +230 -0
  48. package/servers/region-picker/manifest.json +16 -0
  49. package/servers/region-picker/package.json +15 -0
  50. package/src/app.js +1007 -0
  51. package/src/copy-tpl.js +77 -0
  52. package/src/lib/accelerator-validator.js +39 -0
  53. package/src/lib/asset-manager.js +385 -0
  54. package/src/lib/aws-profile-parser.js +181 -0
  55. package/src/lib/bootstrap-command-handler.js +1647 -0
  56. package/src/lib/bootstrap-config.js +238 -0
  57. package/src/lib/ci-register-helpers.js +124 -0
  58. package/src/lib/ci-report-helpers.js +158 -0
  59. package/src/lib/ci-stage-helpers.js +268 -0
  60. package/src/lib/cli-handler.js +529 -0
  61. package/src/lib/comment-generator.js +544 -0
  62. package/src/lib/community-reports-validator.js +91 -0
  63. package/src/lib/config-manager.js +2106 -0
  64. package/src/lib/configuration-exporter.js +204 -0
  65. package/src/lib/configuration-manager.js +695 -0
  66. package/src/lib/configuration-matcher.js +221 -0
  67. package/src/lib/cpu-validator.js +36 -0
  68. package/src/lib/cuda-validator.js +57 -0
  69. package/src/lib/deployment-config-resolver.js +103 -0
  70. package/src/lib/deployment-entry-schema.js +125 -0
  71. package/src/lib/deployment-registry.js +598 -0
  72. package/src/lib/docker-introspection-validator.js +51 -0
  73. package/src/lib/engine-prefix-resolver.js +60 -0
  74. package/src/lib/huggingface-client.js +172 -0
  75. package/src/lib/key-value-parser.js +37 -0
  76. package/src/lib/known-flags-validator.js +200 -0
  77. package/src/lib/manifest-cli.js +280 -0
  78. package/src/lib/mcp-client.js +303 -0
  79. package/src/lib/mcp-command-handler.js +532 -0
  80. package/src/lib/neuron-validator.js +80 -0
  81. package/src/lib/parameter-schema-validator.js +284 -0
  82. package/src/lib/prompt-runner.js +1349 -0
  83. package/src/lib/prompts.js +1138 -0
  84. package/src/lib/registry-command-handler.js +519 -0
  85. package/src/lib/registry-loader.js +198 -0
  86. package/src/lib/rocm-validator.js +80 -0
  87. package/src/lib/schema-validator.js +157 -0
  88. package/src/lib/sensitive-redactor.js +59 -0
  89. package/src/lib/template-engine.js +156 -0
  90. package/src/lib/template-manager.js +341 -0
  91. package/src/lib/validation-engine.js +314 -0
  92. package/src/prompt-adapter.js +63 -0
  93. package/templates/Dockerfile +300 -0
  94. package/templates/IAM_PERMISSIONS.md +84 -0
  95. package/templates/MIGRATION.md +488 -0
  96. package/templates/PROJECT_README.md +439 -0
  97. package/templates/TEMPLATE_SYSTEM.md +243 -0
  98. package/templates/buildspec.yml +64 -0
  99. package/templates/code/chat_template.jinja +1 -0
  100. package/templates/code/flask/gunicorn_config.py +35 -0
  101. package/templates/code/flask/wsgi.py +10 -0
  102. package/templates/code/model_handler.py +387 -0
  103. package/templates/code/serve +300 -0
  104. package/templates/code/serve.py +175 -0
  105. package/templates/code/serving.properties +105 -0
  106. package/templates/code/start_server.py +39 -0
  107. package/templates/code/start_server.sh +39 -0
  108. package/templates/diffusors/Dockerfile +72 -0
  109. package/templates/diffusors/patch_image_api.py +35 -0
  110. package/templates/diffusors/serve +115 -0
  111. package/templates/diffusors/start_server.sh +114 -0
  112. package/templates/do/.gitkeep +1 -0
  113. package/templates/do/README.md +541 -0
  114. package/templates/do/build +83 -0
  115. package/templates/do/ci +681 -0
  116. package/templates/do/clean +811 -0
  117. package/templates/do/config +260 -0
  118. package/templates/do/deploy +1560 -0
  119. package/templates/do/export +306 -0
  120. package/templates/do/logs +319 -0
  121. package/templates/do/manifest +12 -0
  122. package/templates/do/push +119 -0
  123. package/templates/do/register +580 -0
  124. package/templates/do/run +113 -0
  125. package/templates/do/submit +417 -0
  126. package/templates/do/test +1147 -0
  127. package/templates/hyperpod/configmap.yaml +24 -0
  128. package/templates/hyperpod/deployment.yaml +71 -0
  129. package/templates/hyperpod/pvc.yaml +42 -0
  130. package/templates/hyperpod/service.yaml +17 -0
  131. package/templates/nginx-diffusors.conf +74 -0
  132. package/templates/nginx-predictors.conf +47 -0
  133. package/templates/nginx-tensorrt.conf +74 -0
  134. package/templates/requirements.txt +61 -0
  135. package/templates/sample_model/test_inference.py +123 -0
  136. package/templates/sample_model/train_abalone.py +252 -0
  137. package/templates/test/test_endpoint.sh +79 -0
  138. package/templates/test/test_local_image.sh +80 -0
  139. package/templates/test/test_model_handler.py +180 -0
  140. package/templates/triton/Dockerfile +128 -0
  141. package/templates/triton/config.pbtxt +163 -0
  142. package/templates/triton/model.py +130 -0
  143. package/templates/triton/requirements.txt +11 -0
@@ -0,0 +1,38 @@
1
+ [
2
+ {
3
+ "image": "python:3.12-slim",
4
+ "tag": "3.12-slim",
5
+ "architecture": "amd64",
6
+ "created": "2024-10-01T00:00:00Z",
7
+ "labels": { "python_version": "3.12" },
8
+ "registry": "dockerhub",
9
+ "repository": "python"
10
+ },
11
+ {
12
+ "image": "python:3.11-slim",
13
+ "tag": "3.11-slim",
14
+ "architecture": "amd64",
15
+ "created": "2023-10-01T00:00:00Z",
16
+ "labels": { "python_version": "3.11" },
17
+ "registry": "dockerhub",
18
+ "repository": "python"
19
+ },
20
+ {
21
+ "image": "python:3.10-slim",
22
+ "tag": "3.10-slim",
23
+ "architecture": "amd64",
24
+ "created": "2022-10-01T00:00:00Z",
25
+ "labels": { "python_version": "3.10" },
26
+ "registry": "dockerhub",
27
+ "repository": "python"
28
+ },
29
+ {
30
+ "image": "python:3.9-slim",
31
+ "tag": "3.9-slim",
32
+ "architecture": "amd64",
33
+ "created": "2021-10-01T00:00:00Z",
34
+ "labels": { "python_version": "3.9" },
35
+ "registry": "dockerhub",
36
+ "repository": "python"
37
+ }
38
+ ]
@@ -0,0 +1,51 @@
1
+ {
2
+ "fil": {
3
+ "requiresGpu": false,
4
+ "modelFormats": ["xgboost_json", "xgboost_ubj", "lightgbm_txt"],
5
+ "modelArtifactName": "xgboost.json",
6
+ "requiresModelName": false,
7
+ "supportsSampleModel": true
8
+ },
9
+ "onnxruntime": {
10
+ "requiresGpu": false,
11
+ "modelFormats": ["onnx"],
12
+ "modelArtifactName": "model.onnx",
13
+ "requiresModelName": false,
14
+ "supportsSampleModel": true
15
+ },
16
+ "tensorflow": {
17
+ "requiresGpu": false,
18
+ "modelFormats": ["savedmodel"],
19
+ "modelArtifactName": "model.savedmodel/",
20
+ "requiresModelName": false,
21
+ "supportsSampleModel": true
22
+ },
23
+ "pytorch": {
24
+ "requiresGpu": false,
25
+ "modelFormats": ["torchscript"],
26
+ "modelArtifactName": "model.pt",
27
+ "requiresModelName": false,
28
+ "supportsSampleModel": false
29
+ },
30
+ "vllm": {
31
+ "requiresGpu": true,
32
+ "modelFormats": null,
33
+ "modelArtifactName": null,
34
+ "requiresModelName": true,
35
+ "supportsSampleModel": false
36
+ },
37
+ "tensorrtllm": {
38
+ "requiresGpu": true,
39
+ "modelFormats": null,
40
+ "modelArtifactName": null,
41
+ "requiresModelName": true,
42
+ "supportsSampleModel": false
43
+ },
44
+ "python": {
45
+ "requiresGpu": false,
46
+ "modelFormats": ["pkl", "joblib", "custom"],
47
+ "modelArtifactName": "model.py",
48
+ "requiresModelName": false,
49
+ "supportsSampleModel": true
50
+ }
51
+ }
@@ -0,0 +1,38 @@
1
+ [
2
+ {
3
+ "image": "nvcr.io/nvidia/tritonserver:24.08-py3",
4
+ "tag": "24.08-py3",
5
+ "architecture": "amd64",
6
+ "created": "2024-08-15T00:00:00Z",
7
+ "labels": { "cuda_version": "12.5", "python_version": "3.10", "triton_version": "24.08" },
8
+ "registry": "ngc",
9
+ "repository": "nvidia/tritonserver"
10
+ },
11
+ {
12
+ "image": "nvcr.io/nvidia/tritonserver:24.05-py3",
13
+ "tag": "24.05-py3",
14
+ "architecture": "amd64",
15
+ "created": "2024-05-15T00:00:00Z",
16
+ "labels": { "cuda_version": "12.4", "python_version": "3.10", "triton_version": "24.05" },
17
+ "registry": "ngc",
18
+ "repository": "nvidia/tritonserver"
19
+ },
20
+ {
21
+ "image": "nvcr.io/nvidia/tritonserver:24.01-py3",
22
+ "tag": "24.01-py3",
23
+ "architecture": "amd64",
24
+ "created": "2024-01-15T00:00:00Z",
25
+ "labels": { "cuda_version": "12.3", "python_version": "3.10", "triton_version": "24.01" },
26
+ "registry": "ngc",
27
+ "repository": "nvidia/tritonserver"
28
+ },
29
+ {
30
+ "image": "nvcr.io/nvidia/tritonserver:23.10-py3",
31
+ "tag": "23.10-py3",
32
+ "architecture": "amd64",
33
+ "created": "2023-10-15T00:00:00Z",
34
+ "labels": { "cuda_version": "12.2", "python_version": "3.10", "triton_version": "23.10" },
35
+ "registry": "ngc",
36
+ "repository": "nvidia/tritonserver"
37
+ }
38
+ ]
@@ -0,0 +1,495 @@
1
+ #!/usr/bin/env node
2
+ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ /**
6
+ * Base Image Picker MCP Server
7
+ *
8
+ * A bundled MCP server that returns curated base container images for all
9
+ * frameworks. For transformer serving frameworks (vLLM, SGLang, TensorRT-LLM,
10
+ * LMI, DJL), it returns framework-specific serving images from a static catalog.
11
+ * For non-transformer frameworks (sklearn, xgboost, tensorflow), it returns
12
+ * python:3.x-slim images with optional search filtering.
13
+ *
14
+ * Uses a pluggable ImageResolver architecture. V1 ships with StaticCatalogResolver.
15
+ *
16
+ * Tool: get_base_images
17
+ * Accepts: { parameters: string[], limit: number, context: object }
18
+ * Returns: { values, choices, metadata }
19
+ */
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'
28
+
29
+ // ── Catalog loader ───────────────────────────────────────────────────────────
30
+
31
+ const __filename = fileURLToPath(import.meta.url)
32
+ const __dirname = dirname(__filename)
33
+
34
+ /**
35
+ * Load and parse a JSON catalog file relative to the server directory.
36
+ * Throws on missing file or invalid JSON with the file path in the message.
37
+ *
38
+ * @param {string} relativePath - Path relative to server dir (e.g. './catalogs/regions.json')
39
+ * @returns {any} Parsed JSON content
40
+ */
41
+ function loadCatalog(relativePath) {
42
+ const fullPath = resolve(__dirname, relativePath)
43
+ let raw
44
+ try {
45
+ raw = readFileSync(fullPath, 'utf8')
46
+ } catch (err) {
47
+ throw new Error(`Catalog file not found: ${fullPath}`)
48
+ }
49
+ try {
50
+ return JSON.parse(raw)
51
+ } catch (err) {
52
+ throw new Error(`Failed to parse catalog ${fullPath}: ${err.message}`)
53
+ }
54
+ }
55
+
56
+ // ── ImageResolver interface ──────────────────────────────────────────────────
57
+
58
+ /**
59
+ * ImageResolver — image-specific dynamic resolver.
60
+ *
61
+ * Extends DynamicResolver with image-specific method names (fetchImages, supportedFrameworks)
62
+ * that delegate to the generic fetch/supportedKeys interface.
63
+ *
64
+ * Each resolver knows how to fetch images from a specific registry source.
65
+ * The MCP server delegates to the appropriate resolver based on framework/modelServer.
66
+ */
67
+ class ImageResolver extends DynamicResolverBase {
68
+ /**
69
+ * Fetch available images for a given framework.
70
+ *
71
+ * @param {string} framework - The framework identifier (e.g., 'vllm', 'python-slim')
72
+ * @param {object} options - Resolver-specific options
73
+ * @param {number} options.limit - Maximum number of images to return
74
+ * @param {string} [options.searchCriteria] - Optional filter string
75
+ * @returns {Promise<{images: object[], defaultImage: string|null}>}
76
+ */
77
+ async fetchImages(framework, options = {}) {
78
+ throw new Error('fetchImages() must be implemented by subclass')
79
+ }
80
+
81
+ /**
82
+ * Returns the list of framework identifiers this resolver can handle.
83
+ * @returns {string[]}
84
+ */
85
+ supportedFrameworks() {
86
+ throw new Error('supportedFrameworks() must be implemented by subclass')
87
+ }
88
+
89
+ // ── DynamicResolver interface bridge ─────────────────────────────────
90
+
91
+ async fetch(key, options = {}) {
92
+ return this.fetchImages(key, options)
93
+ }
94
+
95
+ supportedKeys() {
96
+ return this.supportedFrameworks()
97
+ }
98
+ }
99
+
100
+ // ── Load catalogs from JSON files ─────────────────────────────────────────────
101
+
102
+ let TRANSFORMER_IMAGE_CATALOG
103
+ let PYTHON_SLIM_CATALOG
104
+ let TRITON_IMAGE_CATALOG
105
+
106
+ try {
107
+ TRANSFORMER_IMAGE_CATALOG = loadCatalog('./catalogs/model-servers.json')
108
+ PYTHON_SLIM_CATALOG = loadCatalog('./catalogs/python-slim.json')
109
+ TRITON_IMAGE_CATALOG = loadCatalog('./catalogs/triton.json')
110
+ } catch (err) {
111
+ process.stderr.write(`[base-image-picker] Fatal: ${err.message}\n`)
112
+ process.exit(1)
113
+ }
114
+
115
+ // ── DynamicResolver ──────────────────────────────────────────────────────────
116
+
117
+ /**
118
+ * DynamicResolver — fetches recent images from an external registry API.
119
+ *
120
+ * Implements ImageResolver so it can be registered in the ResolverRegistry
121
+ * alongside StaticCatalogResolver. Only activated when --discover flag or
122
+ * MCP_DISCOVER=true is set.
123
+ */
124
+ class DynamicResolver extends ImageResolver {
125
+ constructor(options = {}) {
126
+ super()
127
+ this._timeout = options.timeout || 5000
128
+ // Registry API endpoints per framework
129
+ this._registryEndpoints = {
130
+ 'vllm': 'https://hub.docker.com/v2/repositories/vllm/vllm-openai/tags',
131
+ 'sglang': 'https://hub.docker.com/v2/repositories/lmsysorg/sglang/tags',
132
+ 'djl': 'https://hub.docker.com/v2/repositories/deepjavalibrary/djl-serving/tags'
133
+ // tensorrt-llm and lmi require auth — not supported in V1 discover
134
+ }
135
+ }
136
+
137
+ async fetchImages(framework, options = {}) {
138
+ const { limit = 5 } = options
139
+ const endpoint = this._registryEndpoints[framework]
140
+ if (!endpoint) {
141
+ return { images: [], defaultImage: null }
142
+ }
143
+
144
+ const controller = new AbortController()
145
+ const timer = setTimeout(() => controller.abort(), this._timeout)
146
+
147
+ try {
148
+ const response = await fetch(
149
+ `${endpoint}?page_size=${limit}&ordering=-last_updated`,
150
+ { signal: controller.signal }
151
+ )
152
+ clearTimeout(timer)
153
+
154
+ if (!response.ok) {
155
+ throw new Error(`Registry API returned ${response.status}`)
156
+ }
157
+
158
+ const data = await response.json()
159
+ const images = (data.results || []).map(tag => ({
160
+ image: `${this._repoForFramework(framework)}:${tag.name}`,
161
+ tag: tag.name,
162
+ architecture: 'amd64',
163
+ created: tag.last_updated || tag.tag_last_pushed || new Date().toISOString(),
164
+ labels: {},
165
+ registry: 'dockerhub',
166
+ repository: this._repoForFramework(framework)
167
+ }))
168
+
169
+ return {
170
+ images: images.slice(0, limit),
171
+ defaultImage: images[0]?.image || null
172
+ }
173
+ } catch (err) {
174
+ log(`[discover] Registry API failed for ${framework}: ${err.message}`)
175
+ return { images: [], defaultImage: null }
176
+ } finally {
177
+ clearTimeout(timer)
178
+ }
179
+ }
180
+
181
+ supportedFrameworks() {
182
+ return Object.keys(this._registryEndpoints)
183
+ }
184
+
185
+ _repoForFramework(framework) {
186
+ const map = {
187
+ 'vllm': 'vllm/vllm-openai',
188
+ 'sglang': 'lmsysorg/sglang',
189
+ 'djl': 'deepjavalibrary/djl-serving'
190
+ }
191
+ return map[framework] || framework
192
+ }
193
+ }
194
+
195
+ // ── Merge logic ──────────────────────────────────────────────────────────────
196
+
197
+ /**
198
+ * Merge static catalog entries with dynamic registry results.
199
+ *
200
+ * Rules:
201
+ * (a) Static entries come first, in their original order
202
+ * (b) No duplicate image identifiers — static takes precedence
203
+ * (c) Net-new dynamic entries follow, sorted by created date descending
204
+ * (d) Result is capped at `limit`
205
+ *
206
+ * @param {object[]} staticImages - Static catalog entries (original order)
207
+ * @param {object[]} dynamicImages - Dynamic registry entries
208
+ * @param {number} [limit] - Optional cap on total results
209
+ * @returns {object[]} Merged, deduplicated image list
210
+ */
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))
214
+
215
+ // Sort net-new by created desc
216
+ netNew.sort((a, b) => new Date(b.created) - new Date(a.created))
217
+
218
+ const merged = [...staticImages, ...netNew]
219
+ return limit != null ? merged.slice(0, limit) : merged
220
+ }
221
+
222
+ // ── StaticCatalogResolver ────────────────────────────────────────────────────
223
+
224
+ /**
225
+ * StaticCatalogResolver — V1 implementation.
226
+ *
227
+ * Returns images from the externalized catalog JSON files.
228
+ * No network calls, no auth, no external dependencies.
229
+ */
230
+ class StaticCatalogResolver extends ImageResolver {
231
+ constructor(transformerCatalog, pythonSlimCatalog, tritonCatalog) {
232
+ super()
233
+ this._transformerCatalog = transformerCatalog
234
+ this._pythonSlimCatalog = pythonSlimCatalog
235
+ this._tritonCatalog = tritonCatalog || []
236
+ }
237
+
238
+ async fetchImages(framework, options = {}) {
239
+ const { limit = 5, searchCriteria } = options
240
+
241
+ if (framework === 'python-slim') {
242
+ return this._resolvePythonSlim(limit, searchCriteria)
243
+ }
244
+
245
+ if (framework === 'triton') {
246
+ return this._resolveTriton(limit, searchCriteria)
247
+ }
248
+
249
+ const catalog = this._transformerCatalog[framework] || []
250
+ const sliced = catalog.slice(0, limit)
251
+ return {
252
+ images: sliced,
253
+ defaultImage: sliced[0]?.image || null
254
+ }
255
+ }
256
+
257
+ supportedFrameworks() {
258
+ return [
259
+ ...Object.keys(this._transformerCatalog),
260
+ 'python-slim',
261
+ 'triton'
262
+ ]
263
+ }
264
+
265
+ _resolvePythonSlim(limit, searchCriteria) {
266
+ let catalog = [...this._pythonSlimCatalog]
267
+
268
+ if (searchCriteria && searchCriteria.trim()) {
269
+ const query = searchCriteria.trim().toLowerCase()
270
+ catalog = catalog.filter(entry =>
271
+ entry.tag.toLowerCase().includes(query) ||
272
+ entry.image.toLowerCase().includes(query) ||
273
+ (entry.labels.python_version && entry.labels.python_version.toLowerCase().includes(query))
274
+ )
275
+ }
276
+
277
+ const sliced = catalog.slice(0, limit)
278
+ return {
279
+ images: sliced,
280
+ defaultImage: sliced[0]?.image || null
281
+ }
282
+ }
283
+
284
+ _resolveTriton(limit, searchCriteria) {
285
+ let catalog = [...this._tritonCatalog]
286
+
287
+ if (searchCriteria && searchCriteria.trim()) {
288
+ const query = searchCriteria.trim().toLowerCase()
289
+ catalog = catalog.filter(entry =>
290
+ entry.tag.toLowerCase().includes(query) ||
291
+ entry.image.toLowerCase().includes(query) ||
292
+ (entry.labels.triton_version && entry.labels.triton_version.toLowerCase().includes(query)) ||
293
+ (entry.labels.cuda_version && entry.labels.cuda_version.toLowerCase().includes(query))
294
+ )
295
+ }
296
+
297
+ const sliced = catalog.slice(0, limit)
298
+ return {
299
+ images: sliced,
300
+ defaultImage: sliced[0]?.image || null
301
+ }
302
+ }
303
+ }
304
+
305
+ // ── ResolverRegistry ─────────────────────────────────────────────────────────
306
+
307
+ /**
308
+ * ResolverRegistry — maps framework identifiers to their ImageResolver.
309
+ *
310
+ * V1: All frameworks → StaticCatalogResolver
311
+ * Future: Each framework → its appropriate dynamic resolver
312
+ */
313
+ class ResolverRegistry {
314
+ constructor() {
315
+ this._resolvers = new Map()
316
+ this._defaultResolver = null
317
+ }
318
+
319
+ /**
320
+ * Register a resolver for all its supported frameworks.
321
+ * @param {ImageResolver} resolver
322
+ */
323
+ register(resolver) {
324
+ for (const framework of resolver.supportedFrameworks()) {
325
+ this._resolvers.set(framework, resolver)
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Set the fallback resolver used when no framework-specific resolver is found.
331
+ * @param {ImageResolver} resolver
332
+ */
333
+ setDefault(resolver) {
334
+ this._defaultResolver = resolver
335
+ }
336
+
337
+ /**
338
+ * Get the resolver for a given framework.
339
+ * @param {string} framework
340
+ * @returns {ImageResolver|null}
341
+ */
342
+ getResolver(framework) {
343
+ return this._resolvers.get(framework) || this._defaultResolver
344
+ }
345
+ }
346
+
347
+ // ── V1 wiring ────────────────────────────────────────────────────────────────
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)
353
+
354
+ // ── Discover mode ────────────────────────────────────────────────────────────
355
+
356
+ /**
357
+ * Detect discover mode from CLI flag or environment variable.
358
+ * --discover flag or MCP_DISCOVER=true activates discover mode.
359
+ */
360
+ const discoverMode = process.argv.includes('--discover') ||
361
+ process.env.MCP_DISCOVER === 'true'
362
+
363
+ let dynamicResolver = null
364
+
365
+ if (discoverMode) {
366
+ dynamicResolver = new DynamicResolver()
367
+ registry.register(dynamicResolver)
368
+ }
369
+
370
+ // ── Routing logic ────────────────────────────────────────────────────────────
371
+
372
+ /**
373
+ * Resolve base images based on context.
374
+ * Routes transformer frameworks by modelServer, non-transformers to python-slim.
375
+ * When discover mode is active, merges static and dynamic results.
376
+ */
377
+ async function resolveBaseImage(context, limit) {
378
+ const { framework, modelServer, searchCriteria, architecture } = context
379
+
380
+ // Determine which framework identifier to resolve
381
+ let resolverKey
382
+ if (architecture === 'triton') {
383
+ resolverKey = 'triton'
384
+ } else if (architecture === 'diffusors' && modelServer) {
385
+ resolverKey = modelServer
386
+ } else if (framework === 'transformers' && modelServer) {
387
+ resolverKey = modelServer
388
+ } else {
389
+ resolverKey = 'python-slim'
390
+ }
391
+
392
+ const resolver = registry.getResolver(resolverKey)
393
+ if (!resolver) {
394
+ return { values: { baseImage: null }, choices: { baseImage: [] }, metadata: { baseImage: [] } }
395
+ }
396
+
397
+ let resultImages
398
+
399
+ if (discoverMode && dynamicResolver && dynamicResolver.supportedFrameworks().includes(resolverKey)) {
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 })
403
+
404
+ resultImages = mergeStaticAndDynamic(staticResult.images, dynamicResult.images, limit)
405
+ } else {
406
+ // Static-only path (no network calls)
407
+ const result = await resolver.fetchImages(resolverKey, { limit, searchCriteria })
408
+ resultImages = result.images
409
+ }
410
+
411
+ const images = resultImages.map(e => e.image)
412
+ return {
413
+ values: { baseImage: images[0] || null },
414
+ choices: { baseImage: images },
415
+ metadata: { baseImage: resultImages }
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Log to stderr so it doesn't interfere with MCP stdio protocol on stdout.
421
+ */
422
+ function log(message) {
423
+ process.stderr.write(`[base-image-picker] ${message}\n`)
424
+ }
425
+
426
+ // ── MCP Server ───────────────────────────────────────────────────────────────
427
+
428
+ const server = new McpServer({
429
+ name: 'base-image-picker',
430
+ version: '1.0.0'
431
+ })
432
+
433
+ server.tool(
434
+ 'get_base_images',
435
+ 'Returns curated base container images for ML Container Creator Dockerfiles',
436
+ {
437
+ parameters: z.array(z.string()).describe('List of parameter names to provide values for'),
438
+ limit: z.number().int().positive().default(5).describe('Maximum number of choices per parameter'),
439
+ context: z.record(z.string(), z.any()).optional().describe('Current configuration context (framework, modelServer, searchCriteria)')
440
+ },
441
+ async ({ parameters, limit, context }) => {
442
+ const values = {}
443
+ const choices = {}
444
+ const metadata = {}
445
+
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)
451
+ }
452
+
453
+ return {
454
+ content: [{
455
+ type: 'text',
456
+ text: JSON.stringify({ values, choices, metadata })
457
+ }]
458
+ }
459
+ }
460
+ )
461
+
462
+ // ── Exports for testing ──────────────────────────────────────────────────────
463
+
464
+ export {
465
+ loadCatalog,
466
+ ImageResolver,
467
+ StaticCatalogResolver,
468
+ DynamicResolver,
469
+ ResolverRegistry,
470
+ TRANSFORMER_IMAGE_CATALOG,
471
+ PYTHON_SLIM_CATALOG,
472
+ TRITON_IMAGE_CATALOG,
473
+ resolveBaseImage,
474
+ mergeStaticAndDynamic,
475
+ registry,
476
+ staticResolver,
477
+ dynamicResolver,
478
+ discoverMode
479
+ }
480
+
481
+ export { DynamicResolverBase as DynamicResolverBase }
482
+
483
+ // ── Main guard ───────────────────────────────────────────────────────────────
484
+
485
+ const isMain = process.argv[1] && resolve(process.argv[1]) === __filename
486
+
487
+ if (isMain) {
488
+ if (discoverMode) {
489
+ log('Discover mode — serving curated catalogs + live registry lookups')
490
+ } else {
491
+ log('Static mode — serving curated base image catalogs')
492
+ }
493
+ const transport = new StdioServerTransport()
494
+ await server.connect(transport)
495
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@amzn/ml-container-creator-base-image-picker",
3
+ "version": "1.0.0",
4
+ "description": "MCP server that returns curated base container images for ML Container Creator.",
5
+ "modes": {
6
+ "static": true,
7
+ "smart": false,
8
+ "discover": true
9
+ },
10
+ "catalogs": {
11
+ "model-servers": "./catalogs/model-servers.json",
12
+ "python-slim": "./catalogs/python-slim.json"
13
+ },
14
+ "tool": {
15
+ "name": "get_base_images"
16
+ }
17
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@amzn/ml-container-creator-base-image-picker",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "description": "MCP server that returns curated base container images for ML Container Creator. Supports transformer serving frameworks and Python slim images for traditional ML.",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "license": "Apache-2.0",
9
+ "scripts": {
10
+ "test": "node test.js"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.0.0"
14
+ }
15
+ }