Skip to content

Commit 6ad01bf

Browse files
authored
Merge pull request #254 from dgageot/bundle-icons
Bundle icons
2 parents 6dd3aef + 275b51b commit 6ad01bf

File tree

10 files changed

+111
-42
lines changed

10 files changed

+111
-42
lines changed

src/extension/Dockerfile

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,45 @@
1-
## syntax=docker/dockerfile:1.4
2-
#FROM golang:1.24-alpine AS builder
3-
#ENV CGO_ENABLED=0
4-
#WORKDIR /backend
5-
#COPY backend/go.* .
6-
#RUN --mount=type=cache,target=/go/pkg/mod \
7-
#--mount=type=cache,target=/root/.cache/go-build \
8-
#go mod download
9-
#COPY backend/. .
10-
#RUN --mount=type=cache,target=/go/pkg/mod \
11-
#--mount=type=cache,target=/root/.cache/go-build \
12-
#go build -trimpath -ldflags="-s -w" -o bin/service
1+
# syntax=docker/dockerfile:1
132

14-
FROM --platform=$BUILDPLATFORM node:23-alpine3.20 AS client-builder
3+
FROM alpine:3.21@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c AS alpine
4+
5+
FROM --platform=$BUILDPLATFORM node:23-alpine3.21@sha256:86703151a18fcd06258e013073508c4afea8e19cd7ed451554221dd00aea83fc AS client-builder
156
WORKDIR /ui
167
COPY ui/package.json ui/package-lock.json ./
17-
RUN --mount=type=cache,target=/usr/src/app/.npm \
18-
npm set cache /usr/src/app/.npm && \
19-
npm ci
8+
RUN --mount=type=cache,target=/root/.npm npm ci
209
COPY ui/. .
21-
RUN npm run build
10+
RUN --mount=type=cache,target=/root/.npm npm run build
11+
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
2217

23-
FROM alpine:3.20
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
2427
ARG TARGETARCH
2528
LABEL org.opencontainers.image.title="Docker MCP Toolkit" \
26-
org.opencontainers.image.description="Docker MCP Toolkit is a Docker Desktop Extension allowing to connect dockerized MCP servers to MCP Clients" \
29+
org.opencontainers.image.description="Docker MCP Toolkit is a Docker Desktop Extension allowing to connect dockerized MCP servers to MCP clients" \
2730
org.opencontainers.image.vendor="Docker Inc" \
2831
com.docker.desktop.extension.api.version="0.3.4" \
2932
com.docker.extension.screenshots='[{"alt":"screenshot of the extension UI", "url":"https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/refs/heads/main/src/extension/ui/src/assets/screenshots/screenshot1.png"}, {"alt":"Screenshot of Tile configuration", "url":"https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/refs/heads/main/src/extension/ui/src/assets/screenshots/screenshot2.png"}, {"alt":"Screenshot of MCP Client configuration", "url":"https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/refs/heads/main/src/extension/ui/src/assets/screenshots/screenshot3.png"}]' \
3033
com.docker.desktop.extension.icon="https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/main/src/extension/ui/src/assets/extension-icon.png" \
31-
com.docker.extension.detailed-description="Browse the Docker MCP Catalog and connect Dockerized MCP servers to your favorite MCP Client" \
34+
com.docker.extension.detailed-description="Browse and connect Dockerized MCP servers to your favorite MCP clients" \
3235
com.docker.extension.publisher-url="https://www.docker.com/" \
3336
com.docker.extension.additional-urls="https://hub.docker.com/catalogs/mcp" \
3437
com.docker.extension.categories="utility-tools" \
3538
com.docker.extension.changelog="Added MCP catalog"
3639

37-
#COPY --from=builder /backend/bin/service /
38-
COPY docker-compose.yaml .
39-
COPY metadata.json .
40-
COPY extension-icon.svg /extension-icon.svg
40+
COPY docker-compose.yaml metadata.json extension-icon.svg /
4141
COPY host-binary/dist/windows-${TARGETARCH}/host-binary.exe /windows/host-binary.exe
4242
COPY host-binary/dist/darwin-${TARGETARCH}/host-binary /darwin/host-binary
4343
COPY host-binary/dist/linux-${TARGETARCH}/host-binary /linux/host-binary
44+
COPY --from=pull-catalog-images /icons ui/static-assets
4445
COPY --from=client-builder /ui/build ui
45-
#COPY data /data
46-
47-
#CMD ["/service", "-socket", "/run/guest-services/backend.sock"]

src/extension/Makefile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# IMAGE?=docker/labs-ai-tools-for-devs
21
IMAGE?=docker/labs-ai-tools-for-devs
32
TAG?=0.2.60
43

@@ -15,7 +14,7 @@ cross:
1514
cd host-binary && $(MAKE) cross
1615

1716
build-extension: cross ## Build service image to be deployed as a desktop extension
18-
docker buildx build --load --tag=$(IMAGE):$(TAG) .
17+
docker buildx build --build-context prompts=../../prompts --load --tag=$(IMAGE):$(TAG) .
1918

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

2928
push-extension: prepare-buildx cross ## Build & Upload extension image to hub. Do not push if tag already exists: make push-extension tag=0.1
30-
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) .
3130

3231
help: ## Show this help
3332
@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: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { useSecrets } from '../../queries/useSecrets';
4545
import { CatalogItemRichened } from '../../types/catalog';
4646
import ConfigEditor from './ConfigEditor';
4747
import { isEmpty } from 'lodash-es';
48+
import getCatalogIconPath from '../../utils/getCatalogIconPath';
4849

4950
interface TabPanelProps {
5051
children?: React.ReactNode;
@@ -122,6 +123,15 @@ const ConfigurationModal = ({
122123
}
123124
}, [catalogItem.canRegister]);
124125

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+
125135
const toolChipStyle = {
126136
padding: '2px 8px',
127137
justifyContent: 'center',
@@ -172,16 +182,19 @@ const ConfigurationModal = ({
172182
alignItems: 'center',
173183
}}
174184
>
175-
<Avatar
176-
variant="square"
177-
src={catalogItem.icon}
178-
alt={catalogItem.name}
179-
sx={{
180-
width: 40,
181-
height: 40,
182-
borderRadius: 1,
183-
}}
184-
/>
185+
{
186+
// TODO: Figure out if catalog icon is actually optional, and if so, find a good fallback.
187+
catalogItem.icon && <Avatar
188+
variant="square"
189+
src={image}
190+
alt={catalogItem.name}
191+
sx={{
192+
width: 40,
193+
height: 40,
194+
borderRadius: 1,
195+
}}
196+
/>
197+
}
185198
{catalogItem.title ?? catalogItem.name}
186199
<Tooltip
187200
placement="right"
@@ -386,7 +399,7 @@ const ConfigurationModal = ({
386399
const secretEdited =
387400
(secret.assigned &&
388401
localSecrets[secret.name] !==
389-
ASSIGNED_SECRET_PLACEHOLDER) ||
402+
ASSIGNED_SECRET_PLACEHOLDER) ||
390403
(!secret.assigned &&
391404
localSecrets[secret.name] !== '');
392405
return (

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Avatar, CardHeader, Switch, Tooltip, Typography } from '@mui/material';
22

33
import { CatalogItemRichened } from '../../types/catalog';
44
import { useEffect, useState } from 'react';
5+
import getCatalogIconPath from '../../utils/getCatalogIconPath';
56

67
type TopProps = {
78
onToggleRegister: (checked: boolean) => void;
@@ -20,13 +21,22 @@ export default function Top({ item, onToggleRegister }: TopProps) {
2021
setToggled(item.registered);
2122
}, [item.registered]);
2223

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]);
32+
2333
return (
2434
<CardHeader
2535
sx={{ padding: 0 }}
2636
avatar={
2737
<Avatar
2838
variant="square"
29-
src={item.icon}
39+
src={image}
3040
alt={item.name}
3141
sx={{
3242
width: 24,
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
});
32+
}
33+
34+
export default getCatalogIconPath;

src/extension/ui/vite.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default defineConfig({
2323
},
2424
},
2525
},
26+
assetsInclude: ['./static-assets/**/*'],
2627
server: {
2728
port: 3000,
2829
strictPort: true,

0 commit comments

Comments
 (0)