Skip to content

Commit 275b51b

Browse files
committed
Fetch icons a docker build time and fallback to remote url if the icon is not found locally
Signed-off-by: David Gageot <[email protected]>
1 parent 697795d commit 275b51b

File tree

10 files changed

+85
-20
lines changed

10 files changed

+85
-20
lines changed

src/extension/Dockerfile

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
# syntax=docker/dockerfile:1
22

3+
FROM alpine:3.21@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c AS alpine
4+
35
FROM --platform=$BUILDPLATFORM node:23-alpine3.21@sha256:86703151a18fcd06258e013073508c4afea8e19cd7ed451554221dd00aea83fc AS client-builder
46
WORKDIR /ui
57
COPY ui/package.json ui/package-lock.json ./
68
RUN --mount=type=cache,target=/root/.npm npm ci
79
COPY ui/. .
810
RUN --mount=type=cache,target=/root/.npm npm run build
911

10-
FROM alpine:3.21@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
12+
FROM alpine AS pull-catalog-images
13+
RUN apk add --no-cache curl yq
14+
COPY --from=prompts catalog.yaml /
15+
RUN <<EOT
16+
set -eo pipefail
17+
18+
mkdir -p /icons
19+
cat /catalog.yaml | yq -r '.registry[].icon' | while read -r iconUrl; do
20+
name=$(echo -n "$iconUrl" | md5sum | cut -d' ' -f1)
21+
echo "${iconUrl} -> ${name}"
22+
curl -fSl "$iconUrl" -o "/icons/${name}"
23+
done
24+
EOT
25+
26+
FROM alpine
1127
ARG TARGETARCH
1228
LABEL org.opencontainers.image.title="Docker MCP Toolkit" \
1329
org.opencontainers.image.description="Docker MCP Toolkit is a Docker Desktop Extension allowing to connect dockerized MCP servers to MCP clients" \
@@ -22,8 +38,8 @@ LABEL org.opencontainers.image.title="Docker MCP Toolkit" \
2238
com.docker.extension.changelog="Added MCP catalog"
2339

2440
COPY docker-compose.yaml metadata.json extension-icon.svg /
25-
COPY ui/static-assets ui/static-assets
2641
COPY host-binary/dist/windows-${TARGETARCH}/host-binary.exe /windows/host-binary.exe
2742
COPY host-binary/dist/darwin-${TARGETARCH}/host-binary /darwin/host-binary
2843
COPY host-binary/dist/linux-${TARGETARCH}/host-binary /linux/host-binary
44+
COPY --from=pull-catalog-images /icons ui/static-assets
2945
COPY --from=client-builder /ui/build ui

src/extension/Makefile

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,8 @@ bin:
1313
cross:
1414
cd host-binary && $(MAKE) cross
1515

16-
pull-catalog-images: ## Pull all icons from the catalog into static-assets with base16 filename safe for paths and urls alike
17-
cat ../../prompts/catalog.yaml | yq -r '.registry[].icon' | while read -r iconUrl; do \
18-
iconName=$$( echo "$$iconUrl" | xxd -p -u | tr -d '\n' ); \
19-
iconPath="ui/static-assets/$$iconName.png"; \
20-
[ -f "$$iconPath" ] || curl -o "$$iconPath" "$$iconUrl"; \
21-
done
22-
23-
build-extension: cross pull-catalog-images ## Build service image to be deployed as a desktop extension
24-
docker buildx build --load --tag=$(IMAGE):$(TAG) .
16+
build-extension: cross ## Build service image to be deployed as a desktop extension
17+
docker buildx build --build-context prompts=../../prompts --load --tag=$(IMAGE):$(TAG) .
2518

2619
install-extension: build-extension ## Install the extension
2720
echo y | docker extension install $(IMAGE):$(TAG)
@@ -33,7 +26,7 @@ prepare-buildx: ## Create buildx builder for multi-arch build, if not exists
3326
docker buildx inspect $(BUILDER) || docker buildx create --name=$(BUILDER) --driver=docker-container --driver-opt=network=host
3427

3528
push-extension: prepare-buildx cross ## Build & Upload extension image to hub. Do not push if tag already exists: make push-extension tag=0.1
36-
docker pull $(IMAGE):$(TAG) && echo "Failure: Tag already exists" || docker buildx build --push --builder=$(BUILDER) --platform=linux/amd64,linux/arm64 --build-arg TAG=$(TAG) --tag=$(IMAGE):$(TAG) .
29+
docker pull $(IMAGE):$(TAG) && echo "Failure: Tag already exists" || docker buildx build --build-context prompts=../../prompts --push --builder=$(BUILDER) --platform=linux/amd64,linux/arm64 --build-arg TAG=$(TAG) --tag=$(IMAGE):$(TAG) .
3730

3831
help: ## Show this help
3932
@echo Please specify a build target. The choices are:

src/extension/package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/extension/ui/package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/extension/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@mui/material": "6.4.5",
1313
"@tanstack/react-query": "^5.69.0",
1414
"js-base64": "^3.7.7",
15+
"js-md5": "^0.8.3",
1516
"json-schema-library": "^10.0.0-rc7",
1617
"lodash-es": "^4.17.21",
1718
"react": "^18.2.0",

src/extension/ui/src/components/tile/Modal.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ const ConfigurationModal = ({
123123
}
124124
}, [catalogItem.canRegister]);
125125

126+
const [image, setImage] = useState<string | undefined>(undefined);
127+
useEffect(() => {
128+
if (catalogItem.icon) {
129+
getCatalogIconPath(catalogItem.icon).then((icon) => {
130+
setImage(icon);
131+
});
132+
}
133+
}, [catalogItem.icon]);
134+
126135
const toolChipStyle = {
127136
padding: '2px 8px',
128137
justifyContent: 'center',
@@ -177,7 +186,7 @@ const ConfigurationModal = ({
177186
// TODO: Figure out if catalog icon is actually optional, and if so, find a good fallback.
178187
catalogItem.icon && <Avatar
179188
variant="square"
180-
src={getCatalogIconPath(catalogItem.icon)}
189+
src={image}
181190
alt={catalogItem.name}
182191
sx={{
183192
width: 40,

src/extension/ui/src/components/tile/Top.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,22 @@ export default function Top({ item, onToggleRegister }: TopProps) {
2121
setToggled(item.registered);
2222
}, [item.registered]);
2323

24-
const url = getCatalogIconPath(item.icon || '');
24+
const [image, setImage] = useState<string | undefined>(undefined);
25+
useEffect(() => {
26+
if (item.icon) {
27+
getCatalogIconPath(item.icon).then((icon) => {
28+
setImage(icon);
29+
});
30+
}
31+
}, [item.icon]);
2532

2633
return (
2734
<CardHeader
2835
sx={{ padding: 0 }}
2936
avatar={
3037
<Avatar
3138
variant="square"
32-
src={url}
39+
src={image}
3340
alt={item.name}
3441
sx={{
3542
width: 24,
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
1-
const getCatalogIconPath = (iconUrl: string) => {
2-
const iconFilename = [...iconUrl].map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('').toUpperCase() + '0A.png';
3-
return new URL(`/static-assets/${iconFilename}`, import.meta.url).href;
1+
import { md5 } from "js-md5";
2+
3+
const getCatalogIconPath = async (iconUrl: string) => {
4+
try {
5+
var hash = md5.hex(iconUrl);
6+
const hrefIcon = new URL(import.meta.url + `/../../static-assets/${hash}`).href
7+
8+
await checkIfImageExists(hrefIcon);
9+
return hrefIcon
10+
} catch {
11+
return iconUrl
12+
}
13+
}
14+
15+
function checkIfImageExists(url: string) {
16+
return new Promise((resolve, reject) => {
17+
const img = new Image();
18+
img.src = url;
19+
20+
if (img.complete) {
21+
resolve(true)
22+
} else {
23+
img.onload = () => {
24+
resolve(true)
25+
};
26+
27+
img.onerror = () => {
28+
reject(false)
29+
};
30+
}
31+
});
432
}
533

634
export default getCatalogIconPath;

src/extension/ui/static-assets/.gitignore

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)