@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.
- package/LICENSE +202 -0
- package/LICENSE-THIRD-PARTY +68620 -0
- package/NOTICE +2 -0
- package/README.md +106 -0
- package/bin/cli.js +365 -0
- package/config/defaults.json +32 -0
- package/config/presets/transformers-djl.json +26 -0
- package/config/presets/transformers-gpu.json +24 -0
- package/config/presets/transformers-lmi.json +27 -0
- package/package.json +129 -0
- package/servers/README.md +419 -0
- package/servers/base-image-picker/catalogs/model-servers.json +1191 -0
- package/servers/base-image-picker/catalogs/python-slim.json +38 -0
- package/servers/base-image-picker/catalogs/triton-backends.json +51 -0
- package/servers/base-image-picker/catalogs/triton.json +38 -0
- package/servers/base-image-picker/index.js +495 -0
- package/servers/base-image-picker/manifest.json +17 -0
- package/servers/base-image-picker/package.json +15 -0
- package/servers/hyperpod-cluster-picker/LICENSE +202 -0
- package/servers/hyperpod-cluster-picker/index.js +424 -0
- package/servers/hyperpod-cluster-picker/manifest.json +14 -0
- package/servers/hyperpod-cluster-picker/package.json +17 -0
- package/servers/instance-recommender/LICENSE +202 -0
- package/servers/instance-recommender/catalogs/instances.json +852 -0
- package/servers/instance-recommender/index.js +284 -0
- package/servers/instance-recommender/manifest.json +16 -0
- package/servers/instance-recommender/package.json +15 -0
- package/servers/lib/LICENSE +202 -0
- package/servers/lib/bedrock-client.js +160 -0
- package/servers/lib/custom-validators.js +46 -0
- package/servers/lib/dynamic-resolver.js +36 -0
- package/servers/lib/package.json +11 -0
- package/servers/lib/schemas/image-catalog.schema.json +185 -0
- package/servers/lib/schemas/instances.schema.json +124 -0
- package/servers/lib/schemas/manifest.schema.json +64 -0
- package/servers/lib/schemas/model-catalog.schema.json +91 -0
- package/servers/lib/schemas/regions.schema.json +26 -0
- package/servers/lib/schemas/triton-backends.schema.json +51 -0
- package/servers/model-picker/catalogs/jumpstart-public.json +66 -0
- package/servers/model-picker/catalogs/popular-diffusors.json +88 -0
- package/servers/model-picker/catalogs/popular-transformers.json +226 -0
- package/servers/model-picker/index.js +1693 -0
- package/servers/model-picker/manifest.json +18 -0
- package/servers/model-picker/package.json +20 -0
- package/servers/region-picker/LICENSE +202 -0
- package/servers/region-picker/catalogs/regions.json +263 -0
- package/servers/region-picker/index.js +230 -0
- package/servers/region-picker/manifest.json +16 -0
- package/servers/region-picker/package.json +15 -0
- package/src/app.js +1007 -0
- package/src/copy-tpl.js +77 -0
- package/src/lib/accelerator-validator.js +39 -0
- package/src/lib/asset-manager.js +385 -0
- package/src/lib/aws-profile-parser.js +181 -0
- package/src/lib/bootstrap-command-handler.js +1647 -0
- package/src/lib/bootstrap-config.js +238 -0
- package/src/lib/ci-register-helpers.js +124 -0
- package/src/lib/ci-report-helpers.js +158 -0
- package/src/lib/ci-stage-helpers.js +268 -0
- package/src/lib/cli-handler.js +529 -0
- package/src/lib/comment-generator.js +544 -0
- package/src/lib/community-reports-validator.js +91 -0
- package/src/lib/config-manager.js +2106 -0
- package/src/lib/configuration-exporter.js +204 -0
- package/src/lib/configuration-manager.js +695 -0
- package/src/lib/configuration-matcher.js +221 -0
- package/src/lib/cpu-validator.js +36 -0
- package/src/lib/cuda-validator.js +57 -0
- package/src/lib/deployment-config-resolver.js +103 -0
- package/src/lib/deployment-entry-schema.js +125 -0
- package/src/lib/deployment-registry.js +598 -0
- package/src/lib/docker-introspection-validator.js +51 -0
- package/src/lib/engine-prefix-resolver.js +60 -0
- package/src/lib/huggingface-client.js +172 -0
- package/src/lib/key-value-parser.js +37 -0
- package/src/lib/known-flags-validator.js +200 -0
- package/src/lib/manifest-cli.js +280 -0
- package/src/lib/mcp-client.js +303 -0
- package/src/lib/mcp-command-handler.js +532 -0
- package/src/lib/neuron-validator.js +80 -0
- package/src/lib/parameter-schema-validator.js +284 -0
- package/src/lib/prompt-runner.js +1349 -0
- package/src/lib/prompts.js +1138 -0
- package/src/lib/registry-command-handler.js +519 -0
- package/src/lib/registry-loader.js +198 -0
- package/src/lib/rocm-validator.js +80 -0
- package/src/lib/schema-validator.js +157 -0
- package/src/lib/sensitive-redactor.js +59 -0
- package/src/lib/template-engine.js +156 -0
- package/src/lib/template-manager.js +341 -0
- package/src/lib/validation-engine.js +314 -0
- package/src/prompt-adapter.js +63 -0
- package/templates/Dockerfile +300 -0
- package/templates/IAM_PERMISSIONS.md +84 -0
- package/templates/MIGRATION.md +488 -0
- package/templates/PROJECT_README.md +439 -0
- package/templates/TEMPLATE_SYSTEM.md +243 -0
- package/templates/buildspec.yml +64 -0
- package/templates/code/chat_template.jinja +1 -0
- package/templates/code/flask/gunicorn_config.py +35 -0
- package/templates/code/flask/wsgi.py +10 -0
- package/templates/code/model_handler.py +387 -0
- package/templates/code/serve +300 -0
- package/templates/code/serve.py +175 -0
- package/templates/code/serving.properties +105 -0
- package/templates/code/start_server.py +39 -0
- package/templates/code/start_server.sh +39 -0
- package/templates/diffusors/Dockerfile +72 -0
- package/templates/diffusors/patch_image_api.py +35 -0
- package/templates/diffusors/serve +115 -0
- package/templates/diffusors/start_server.sh +114 -0
- package/templates/do/.gitkeep +1 -0
- package/templates/do/README.md +541 -0
- package/templates/do/build +83 -0
- package/templates/do/ci +681 -0
- package/templates/do/clean +811 -0
- package/templates/do/config +260 -0
- package/templates/do/deploy +1560 -0
- package/templates/do/export +306 -0
- package/templates/do/logs +319 -0
- package/templates/do/manifest +12 -0
- package/templates/do/push +119 -0
- package/templates/do/register +580 -0
- package/templates/do/run +113 -0
- package/templates/do/submit +417 -0
- package/templates/do/test +1147 -0
- package/templates/hyperpod/configmap.yaml +24 -0
- package/templates/hyperpod/deployment.yaml +71 -0
- package/templates/hyperpod/pvc.yaml +42 -0
- package/templates/hyperpod/service.yaml +17 -0
- package/templates/nginx-diffusors.conf +74 -0
- package/templates/nginx-predictors.conf +47 -0
- package/templates/nginx-tensorrt.conf +74 -0
- package/templates/requirements.txt +61 -0
- package/templates/sample_model/test_inference.py +123 -0
- package/templates/sample_model/train_abalone.py +252 -0
- package/templates/test/test_endpoint.sh +79 -0
- package/templates/test/test_local_image.sh +80 -0
- package/templates/test/test_model_handler.py +180 -0
- package/templates/triton/Dockerfile +128 -0
- package/templates/triton/config.pbtxt +163 -0
- package/templates/triton/model.py +130 -0
- 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
|
+
}
|