Skip to content

Commit ff35edb

Browse files
authored
Merge pull request #39 from docker/cm/0.1.1
new alerts and modals
2 parents 2ac5a66 + 2389e77 commit ff35edb

File tree

6 files changed

+80
-24
lines changed

6 files changed

+80
-24
lines changed

src/extension/Dockerfile

+1-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ RUN npm run build
1212

1313
FROM alpine
1414
LABEL org.opencontainers.image.title="Labs: AI Tools for Devs" \
15-
org.opencontainers.image.description="Agentic workflows with Dockerized tools" \
15+
org.opencontainers.image.description="MCP Tool Catalog" \
1616
org.opencontainers.image.vendor="Docker Inc" \
1717
com.docker.desktop.extension.api.version="0.3.4" \
1818
com.docker.extension.screenshots='[{"alt":"screenshot of the extension UI", "url":"https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/main/docs/assets/img/dd-ext-screenshot.png"}, {"alt":"screenshot of the extension output", "url":"https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/main/docs/assets/img/dd-extension-output.png"}]' \
@@ -29,5 +29,3 @@ COPY docker.svg /docker.svg
2929
COPY --from=client-builder /ui/build ui
3030

3131
CMD sleep 600
32-
33-

src/extension/Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
IMAGE?=docker/labs-ai-tools-for-devs
2-
TAG?=0.1.0
2+
TAG?=0.1.1
33

44
BUILDER=buildx-multi-arch
55

src/extension/Screenshot1.png

-185 KB
Binary file not shown.

src/extension/Screenshot2.png

-579 KB
Binary file not shown.

src/extension/ui/src/App.tsx

+28-10
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import React, { useEffect, useState } from 'react';
22
import AddIcon from '@mui/icons-material/Add';
33
import { createDockerDesktopClient } from '@docker/extension-api-client';
4-
import { Stack, Typography, Button, ButtonGroup, Grid, debounce, Card, CardContent, IconButton } from '@mui/material';
4+
import { Stack, Typography, Button, ButtonGroup, Grid, debounce, Card, CardContent, IconButton, Alert } from '@mui/material';
55
import { CatalogItem, CatalogItemCard, CatalogItemWithName } from './components/PromptCard';
66
import { parse, stringify } from 'yaml';
77
import { Ref } from './Refs';
88
import { RegistrySyncStatus } from './components/RegistrySyncStatus';
99
import { getRegistry } from './Registry';
1010
import { ClaudeConfigSyncStatus } from './components/ClaudeConfigSyncStatus';
11+
import { FolderOpen, FolderOpenOutlined, FolderOpenRounded, VolumeUp } from '@mui/icons-material';
1112

1213
type RegistryItem = {
1314
ref: string;
@@ -20,7 +21,7 @@ const CATALOG_URL = 'https://raw.githubusercontent.com/docker/labs-ai-tools-for-
2021
export function App() {
2122

2223
const [registryLoaded, setRegistryLoaded] = useState(false);
23-
const [items, setItems] = useState<CatalogItem[]>([]);
24+
const [catalogItems, setCatalogItems] = useState<CatalogItemWithName[]>([]);
2425
const [canRegister, setCanRegister] = useState(false);
2526
const [registryItems, setRegistryItems] = useState<{ [key: string]: { ref: string } }>({});
2627
const [status, setStatus] = useState<{
@@ -31,12 +32,16 @@ export function App() {
3132
message: ''
3233
});
3334

35+
const [hasConfig, setHasConfig] = useState(false);
36+
3437
const loadCatalog = async () => {
3538
setStatus({ status: 'loading', message: 'Grabbing latest prompt catalog...' });
3639
try {
3740
const response = await fetch(CATALOG_URL);
3841
const catalog = await response.text();
39-
setItems(parse(catalog)['registry']);
42+
const items = parse(catalog)['registry'] as { [key: string]: CatalogItem }
43+
const itemsWithName = Object.entries(items).map(([name, item]) => ({ name, ...item }));
44+
setCatalogItems(itemsWithName);
4045
setStatus({ status: 'idle', message: '' });
4146
}
4247
catch (error) {
@@ -50,9 +55,10 @@ export function App() {
5055
setStatus({ status: 'loading', message: 'Grabbing prompt registry...' });
5156
try {
5257
const result = await getRegistry(client)
53-
setRegistryItems(result);
58+
setRegistryItems(result || {});
5459
setStatus({ status: 'idle', message: '' });
5560
setRegistryLoaded(true);
61+
5662
}
5763
catch (error) {
5864
if (error instanceof Error) {
@@ -114,7 +120,9 @@ export function App() {
114120
}
115121
}, []);
116122

117-
123+
const hasOutOfCatalog = catalogItems.length > 0 && Object.keys(registryItems).length > 0 && !Object.keys(registryItems).every((i) =>
124+
catalogItems.some((c) => c.name === i)
125+
)
118126

119127
return (
120128
<div>
@@ -125,17 +133,25 @@ export function App() {
125133
<Button onClick={loadRegistry}>Refresh registry</Button>
126134
</ButtonGroup>
127135
<RegistrySyncStatus registryLoaded={registryLoaded} />
128-
<ClaudeConfigSyncStatus client={client} />
136+
<ClaudeConfigSyncStatus client={client} setHasConfig={setHasConfig} />
129137
</div>
138+
{!hasConfig && Object.keys(registryItems).length > 0 && <Alert severity="warning">
139+
Claude Desktop has not been configured with docker_mcp. Click on the Claude icon to update the configuration.
140+
</Alert>}
141+
{hasOutOfCatalog && <Alert action={<Button startIcon={<FolderOpenRounded />} variant='outlined' color='secondary' onClick={() => {
142+
client.desktopUI.navigate.viewVolume('docker-prompts')
143+
}}>registry.yaml</Button>} severity="info">
144+
<Typography sx={{ width: '100%' }}>You have some prompts registered which are not available in the catalog.</Typography>
145+
</Alert>}
130146
<Grid container spacing={2}>
131-
{Object.entries(items).map(([name, item]) => (
132-
<Grid item xs={12} sm={6} md={4} key={name} flex="1 1 0">
147+
{catalogItems.map((item) => (
148+
<Grid item xs={12} sm={6} md={4} key={item.name} flex="1 1 0">
133149
<CatalogItemCard openUrl={() => {
134150
client.host.openExternal(Ref.fromRef(item.ref).toURL(true))
135151
}}
136-
item={{ name, ...item }}
152+
item={item}
137153
canRegister={canRegister}
138-
registered={Object.keys(registryItems).some((i) => i === name)}
154+
registered={Object.keys(registryItems).some((i) => i === item.name)}
139155
register={registerCatalogItem}
140156
unregister={unregisterCatalogItem}
141157
/>
@@ -155,6 +171,8 @@ export function App() {
155171
</Grid>
156172
</Grid>
157173
</Stack>
174+
175+
158176
</div>
159177
)
160178
}

src/extension/ui/src/components/ClaudeConfigSyncStatus.tsx

+50-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { v1 } from "@docker/extension-api-client-types";
2-
import { Badge, Button, Dialog, DialogContent, DialogContentText, DialogTitle, Typography } from "@mui/material";
2+
import { Badge, Button, Dialog, DialogContent, DialogContentText, DialogTitle, Stack, Typography } from "@mui/material";
33
import { useEffect, useState } from "react";
44
import { writeFilesToHost } from "../FileWatcher";
55
import { trackEvent } from "../Usage";
@@ -50,21 +50,34 @@ const getClaudeConfig = async (client: v1.DockerDesktopClient) => {
5050
return result.stdout
5151
}
5252

53-
export const ClaudeConfigSyncStatus = ({ client }: { client: v1.DockerDesktopClient }) => {
53+
const setNeverShowAgain = (value: boolean) => {
54+
localStorage.setItem('claude-config-sync-status-never-show-again', value.toString())
55+
}
56+
57+
const getNeverShowAgain = () => {
58+
return localStorage.getItem('claude-config-sync-status-never-show-again') === 'true'
59+
}
60+
61+
export const ClaudeConfigSyncStatus = ({ client, setHasConfig }: { client: v1.DockerDesktopClient, setHasConfig: (hasConfig: boolean) => void }) => {
5462
const [claudeConfig, setClaudeConfig] = useState<{ mcpServers: { [key: string]: any } } | null>(null)
5563
const [showConfigModal, setShowConfigModal] = useState<boolean>(false)
64+
const [showRestartModal, setShowRestartModal] = useState<boolean>(false)
5665
const [status, setStatus] = useState<ClaudeConfigStatus>({ state: 'loading', message: '...', color: 'default' })
5766
const [configPath, setConfigPath] = useState<string | null>(null)
5867
useEffect(() => {
5968
const refreshConfig = async () => {
6069
const config = await getClaudeConfig(client)
61-
setClaudeConfig(JSON.parse(config))
70+
const newConfig = JSON.parse(config)
71+
setClaudeConfig(newConfig)
6272
}
6373
refreshConfig()
74+
75+
6476
const interval = setInterval(() => {
6577
refreshConfig()
6678
}, 30000)
6779

80+
6881
return () => {
6982
clearInterval(interval)
7083
}
@@ -74,45 +87,55 @@ export const ClaudeConfigSyncStatus = ({ client }: { client: v1.DockerDesktopCli
7487
setConfigPath(getClaudeConfigPath(client))
7588
}, [client])
7689

90+
7791
useEffect(() => {
7892
if (claudeConfig) {
7993
const servers = claudeConfig.mcpServers
8094
if (!servers) {
8195
setStatus({ state: 'invalid', message: 'No servers found in Claude Desktop Config', color: 'error' })
96+
8297
}
8398
else {
8499
const hasDocker = Object.keys(servers).some(key => key === 'mcp_docker')
85100
if (hasDocker) {
86101
setStatus({ state: 'has docker_mcp', message: 'Claude Desktop Config is valid', color: 'success' })
102+
setHasConfig(true)
87103
} else {
88104
setStatus({ state: 'missing docker_mcp', message: 'No Docker servers found in Claude Desktop Config', color: 'error' })
105+
setHasConfig(false)
89106
}
107+
90108
}
91109
}
92110
}, [claudeConfig])
93111

94-
if (showConfigModal && configPath) {
95-
return <Dialog open={showConfigModal} onClose={() => setShowConfigModal(false)} maxWidth="lg">
112+
return <Badge badgeContent={status.state} color={status.color} sx={{ ml: 4 }}>
113+
<Dialog open={Boolean(showConfigModal && configPath)} onClose={() => setShowConfigModal(false)} maxWidth="lg">
96114
<DialogTitle>Current Claude Desktop Config</DialogTitle>
115+
97116
{status.state === 'has docker_mcp' && <Button onClick={async () => {
98117
trackEvent('claude-config-changed', { action: 'remove' })
99118
// Remove docker_mcp from config
100119
if (claudeConfig && claudeConfig.mcpServers) {
101120
const newConfig = { ...claudeConfig }
102121
delete newConfig.mcpServers.mcp_docker
103122
setClaudeConfig(newConfig)
104-
const result = await writeFilesToHost(client, [{ path: 'config.json', content: JSON.stringify(newConfig, null, 2) }], [{ source: configPath, target: '/claude_desktop_config/config.json' }], '/claude_desktop_config')
105-
123+
await writeFilesToHost(client, [{ path: 'config.json', content: JSON.stringify(newConfig, null, 2) }], [{ source: configPath!, target: '/claude_desktop_config/config.json' }], '/claude_desktop_config')
124+
setShowRestartModal(!getNeverShowAgain())
125+
setShowConfigModal(false)
106126
}
107127
}}>Remove Docker MCP</Button>}
128+
108129
{status.state === 'missing docker_mcp' && <Button onClick={() => {
109130
trackEvent('claude-config-changed', { action: 'add' })
110131
// Add docker_mcp to config
111132
if (claudeConfig && claudeConfig.mcpServers) {
112133
const newConfig = { ...claudeConfig }
113134
newConfig.mcpServers.mcp_docker = DOCKER_MCP_CONFIG
114135
setClaudeConfig(newConfig)
115-
writeFilesToHost(client, [{ path: 'config.json', content: JSON.stringify(newConfig, null, 2) }], [{ source: configPath, target: '/claude_desktop_config/config.json' }], '/claude_desktop_config')
136+
writeFilesToHost(client, [{ path: 'config.json', content: JSON.stringify(newConfig, null, 2) }], [{ source: configPath!, target: '/claude_desktop_config/config.json' }], '/claude_desktop_config')
137+
setShowRestartModal(!getNeverShowAgain())
138+
setShowConfigModal(false)
116139
}
117140
}}>Add Docker MCP</Button>
118141
}
@@ -122,8 +145,25 @@ export const ClaudeConfigSyncStatus = ({ client }: { client: v1.DockerDesktopCli
122145
</DialogContentText>
123146
</DialogContent>
124147
</Dialog >
125-
}
126-
return <Badge badgeContent={status.state} color={status.color} sx={{ ml: 4 }}>
148+
149+
<Dialog open={showRestartModal} onClose={() => setShowRestartModal(false)} >
150+
<DialogTitle>Restart Claude Desktop</DialogTitle>
151+
<DialogContent sx={{ padding: 5, mt: 2 }}>
152+
<Stack direction="column" spacing={3}>
153+
<Typography>
154+
You must manually restart Claude Desktop to apply changes to the config.
155+
</Typography>
156+
<Button onClick={() => {
157+
setShowRestartModal(false)
158+
}}>Close</Button>
159+
160+
<Button onClick={() => {
161+
setShowRestartModal(false)
162+
setNeverShowAgain(true)
163+
}}>Never show this again</Button>
164+
</Stack>
165+
</DialogContent>
166+
</Dialog >
127167
<Button sx={{ width: 150, height: 'auto', cursor: 'pointer' }} onClick={() => { setShowConfigModal(!showConfigModal) }} color='primary' variant="outlined">
128168
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 184 40" fill="currentColor"><path shapeRendering="optimizeQuality" fill="#D97757" d="m7.75 26.27 7.77-4.36.13-.38-.13-.21h-.38l-1.3-.08-4.44-.12-3.85-.16-3.73-.2-.94-.2L0 19.4l.09-.58.79-.53 1.13.1 2.5.17 3.75.26 2.72.16 4.03.42h.64l.09-.26-.22-.16-.17-.16-3.88-2.63-4.2-2.78-2.2-1.6L3.88 11l-.6-.76-.26-1.66L4.1 7.39l1.45.1.37.1 1.47 1.13 3.14 2.43 4.1 3.02.6.5.24-.17.03-.12-.27-.45L13 9.9l-2.38-4.1-1.06-1.7-.28-1.02c-.1-.42-.17-.77-.17-1.2L10.34.21l.68-.22 1.64.22.69.6 1.02 2.33 1.65 3.67 2.56 4.99.75 1.48.4 1.37.15.42h.26v-.24l.21-2.81.39-3.45.38-4.44.13-1.25.62-1.5L23.1.57l.96.46.79 1.13-.11.73-.47 3.05-.92 4.78-.6 3.2h.35l.4-.4 1.62-2.15 2.72-3.4 1.2-1.35 1.4-1.49.9-.71h1.7l1.25 1.86-.56 1.92-1.75 2.22-1.45 1.88-2.08 2.8-1.3 2.24.12.18.31-.03 4.7-1 2.54-.46 3.03-.52 1.37.64.15.65-.54 1.33-3.24.8-3.8.76-5.66 1.34-.07.05.08.1 2.55.24 1.09.06h2.67l4.97.37 1.3.86.78 1.05-.13.8-2 1.02-2.7-.64-6.3-1.5-2.16-.54h-.3v.18l1.8 1.76 3.3 2.98 4.13 3.84.21.95-.53.75-.56-.08-3.63-2.73-1.4-1.23-3.17-2.67h-.21v.28l.73 1.07 3.86 5.8.2 1.78-.28.58-1 .35-1.1-.2L26 33.14l-2.33-3.57-1.88-3.2-.23.13-1.11 11.95-.52.61-1.2.46-1-.76-.53-1.23.53-2.43.64-3.17.52-2.52.47-3.13.28-1.04-.02-.07-.23.03-2.36 3.24-3.59 4.85-2.84 3.04-.68.27-1.18-.61.11-1.09.66-.97 3.93-5 2.37-3.1 1.53-1.79-.01-.26h-.09L6.8 30.56l-1.86.24-.8-.75.1-1.23.38-.4 3.14-2.16Z"></path><path shapeRendering="optimizeQuality" d="M64.48 33.54c-5.02 0-8.45-2.8-10.07-7.11a19.19 19.19 0 0 1-1.23-7.03c0-7.23 3.24-12.25 10.4-12.25 4.81 0 7.78 2.1 9.47 7.11h2.06l-.28-6.91c-2.88-1.86-6.48-2.8-10.86-2.8-6.17 0-11.42 2.76-14.34 7.74a16.77 16.77 0 0 0-2.22 8.65c0 5.53 2.61 10.43 7.51 13.15a17.51 17.51 0 0 0 8.73 2.06c4.78 0 8.57-.91 11.93-2.5l.87-7.62h-2.1c-1.26 3.48-2.76 5.57-5.25 6.68-1.22.55-2.76.83-4.62.83ZM86.13 7.15l.2-3.4h-1.42l-6.32 1.9v1.03l2.8 1.3v23.78c0 1.62-.83 1.98-3 2.25v1.74h10.75v-1.74c-2.18-.27-3-.63-3-2.25V7.16Zm42.75 29h.83l7.27-1.38v-1.78l-1.02-.08c-1.7-.16-2.14-.51-2.14-1.9V18.33l.2-4.07h-1.15l-6.87.99v1.74l.67.12c1.86.27 2.41.79 2.41 2.09v11.3c-1.78 1.38-3.48 2.25-5.5 2.25-2.24 0-3.63-1.14-3.63-3.8V18.34l.2-4.07h-1.18l-6.88.99v1.74l.71.12c1.86.27 2.41.79 2.41 2.09v10.43c0 4.42 2.5 6.52 6.48 6.52 3.04 0 5.53-1.62 7.4-3.87l-.2 3.87ZM108.9 22.08c0-5.65-3-7.82-8.42-7.82-4.78 0-8.25 1.98-8.25 5.26 0 .98.35 1.73 1.06 2.25l3.64-.48c-.16-1.1-.24-1.77-.24-2.05 0-1.86.99-2.8 3-2.8 2.97 0 4.47 2.09 4.47 5.45v1.1l-7.5 2.25c-2.5.68-3.92 1.27-4.87 2.65a5 5 0 0 0-.7 2.8c0 3.2 2.2 5.46 5.96 5.46 2.72 0 5.13-1.23 7.23-3.56.75 2.33 1.9 3.56 3.95 3.56 1.66 0 3.16-.67 4.5-1.98l-.4-1.38c-.58.16-1.14.24-1.73.24-1.15 0-1.7-.91-1.7-2.69v-8.26Zm-9.6 10.87c-2.05 0-3.32-1.19-3.32-3.28 0-1.42.67-2.25 2.1-2.73l6.08-1.93v5.84c-1.94 1.47-3.08 2.1-4.86 2.1Zm63.3 1.82v-1.78l-1.03-.08c-1.7-.16-2.13-.51-2.13-1.9V7.15l.2-3.4h-1.43l-6.32 1.9v1.03l2.8 1.3v7.82a8.83 8.83 0 0 0-5.37-1.54c-6.28 0-11.18 4.78-11.18 11.93 0 5.89 3.52 9.96 9.32 9.96 3 0 5.61-1.46 7.23-3.72l-.2 3.72h.84l7.27-1.38Zm-13.16-18.14c3 0 5.25 1.74 5.25 4.94v9a7.2 7.2 0 0 1-5.21 2.1c-4.3 0-6.48-3.4-6.48-7.94 0-5.1 2.49-8.1 6.44-8.1Zm28.53 4.5c-.56-2.64-2.18-4.14-4.43-4.14-3.36 0-5.69 2.53-5.69 6.16 0 5.37 2.84 8.85 7.43 8.85a8.6 8.6 0 0 0 7.39-4.35l1.34.36c-.6 4.66-4.82 8.14-10 8.14-6.08 0-10.27-4.5-10.27-10.9 0-6.45 4.55-10.99 10.63-10.99 4.54 0 7.74 2.73 8.77 7.47l-15.84 4.86v-2.14l10.67-3.31Z"></path></svg>
129169
</Button>

0 commit comments

Comments
 (0)