Skip to content

Commit a476aba

Browse files
author
colinmcneil
committed
Add gordon mcp button
1 parent 69cd55a commit a476aba

File tree

5 files changed

+226
-4
lines changed

5 files changed

+226
-4
lines changed

src/extension/ui/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getRegistry } from './Registry';
1010
import { ClaudeConfigSyncStatus, setNeverShowAgain } from './components/ClaudeConfigSyncStatus';
1111
import { FolderOpenRounded, } from '@mui/icons-material';
1212
import { ExecResult } from '@docker/extension-api-client-types/dist/v0';
13+
import Gordon from './components/Gordon';
1314

1415
const NEVER_SHOW_AGAIN_KEY = 'registry-sync-never-show-again';
1516

@@ -181,6 +182,7 @@ export function App() {
181182
</ButtonGroup>
182183
<RegistrySyncStatus registryLoaded={registryLoaded} />
183184
<ClaudeConfigSyncStatus client={client} setHasConfig={setHasConfig} />
185+
<Gordon client={client} />
184186
</div>
185187
{!hasConfig && Object.keys(registryItems).length > 0 && <Alert severity="warning">
186188
Claude Desktop has not been configured with docker_mcp. Click on the Claude icon to update the configuration.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { Grid, Card, CardContent, IconButton, CircularProgress, Alert, Stack, Button, Typography } from '@mui/material';
3+
import { CatalogItemWithName, CatalogItemCard, CatalogItem } from './PromptCard';
4+
import AddIcon from '@mui/icons-material/Add';
5+
import { Ref } from '../Refs';
6+
import { v1 } from "@docker/extension-api-client-types";
7+
import { parse, stringify } from 'yaml';
8+
import { getRegistry } from '../Registry';
9+
import { FolderOpenRounded } from '@mui/icons-material';
10+
11+
interface CatalogGridProps {
12+
registryItems: { [key: string]: { ref: string } };
13+
canRegister: boolean;
14+
client: v1.DockerDesktopClient;
15+
onRegistryChange: () => void;
16+
}
17+
18+
const CATALOG_URL = 'https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/refs/heads/main/prompts/catalog.yaml'
19+
20+
21+
export const CatalogGrid: React.FC<CatalogGridProps> = ({
22+
registryItems,
23+
canRegister,
24+
client,
25+
onRegistryChange,
26+
}) => {
27+
const [catalogItems, setCatalogItems] = useState<CatalogItemWithName[]>([]);
28+
const [loading, setLoading] = useState<boolean>(true);
29+
const [error, setError] = useState<string | null>(null);
30+
31+
const loadCatalog = async (showNotification = true) => {
32+
const cachedCatalog = localStorage.getItem('catalog');
33+
try {
34+
const response = await fetch(CATALOG_URL);
35+
const catalog = await response.text();
36+
const items = parse(catalog)['registry'] as { [key: string]: CatalogItem }
37+
const itemsWithName = Object.entries(items).map(([name, item]) => ({ name, ...item }));
38+
setCatalogItems(itemsWithName);
39+
localStorage.setItem('catalog', JSON.stringify(itemsWithName));
40+
if (showNotification) {
41+
client.desktopUI.toast.success('Catalog updated successfully.');
42+
}
43+
}
44+
catch (error) {
45+
if (cachedCatalog) {
46+
setCatalogItems(JSON.parse(cachedCatalog));
47+
}
48+
if (showNotification) {
49+
client.desktopUI.toast.error(`Failed to get latest catalog.${cachedCatalog ? ' Using cached catalog.' : ''}` + error);
50+
}
51+
}
52+
}
53+
54+
const registerCatalogItem = async (item: CatalogItemWithName) => {
55+
try {
56+
const currentRegistry = await getRegistry(client);
57+
const newRegistry = { ...currentRegistry, [item.name]: { ref: item.ref } };
58+
const payload = JSON.stringify({
59+
files: [{
60+
path: 'registry.yaml',
61+
content: stringify({ registry: newRegistry })
62+
}]
63+
})
64+
await client.docker.cli.exec('run', ['--rm', '-v', 'docker-prompts:/docker-prompts', '--workdir', '/docker-prompts', 'vonwig/function_write_files:latest', `'${payload}'`])
65+
client.desktopUI.toast.success('Prompt registered successfully. Restart Claude Desktop to apply.');
66+
onRegistryChange();
67+
68+
}
69+
catch (error) {
70+
client.desktopUI.toast.error('Failed to register prompt: ' + error);
71+
}
72+
}
73+
74+
const unregisterCatalogItem = async (item: CatalogItemWithName) => {
75+
try {
76+
const currentRegistry = await getRegistry(client);
77+
delete currentRegistry[item.name];
78+
const payload = JSON.stringify({
79+
files: [{
80+
path: 'registry.yaml',
81+
content: stringify({ registry: currentRegistry })
82+
}]
83+
})
84+
await client.docker.cli.exec('run', ['--rm', '-v', 'docker-prompts:/docker-prompts', '--workdir', '/docker-prompts', 'vonwig/function_write_files:latest', `'${payload}'`])
85+
client.desktopUI.toast.success('Prompt unregistered successfully. Restart Claude Desktop to apply.');
86+
onRegistryChange();
87+
}
88+
catch (error) {
89+
client.desktopUI.toast.error('Failed to unregister prompt: ' + error)
90+
}
91+
}
92+
93+
useEffect(() => {
94+
const interval = setInterval(loadCatalog, 1000 * 30);
95+
loadCatalog(false);
96+
return () => {
97+
clearInterval(interval);
98+
}
99+
}, []);
100+
101+
const hasOutOfCatalog = catalogItems.length > 0 && Object.keys(registryItems).length > 0 && !Object.keys(registryItems).every((i) =>
102+
catalogItems.some((c) => c.name === i)
103+
)
104+
105+
return (
106+
<Stack spacing={2}>
107+
{hasOutOfCatalog && <Alert action={<Button startIcon={<FolderOpenRounded />} variant='outlined' color='secondary' onClick={() => {
108+
client.desktopUI.navigate.viewVolume('docker-prompts')
109+
}}>registry.yaml</Button>} severity="info">
110+
<Typography sx={{ width: '100%' }}>You have some prompts registered which are not available in the catalog.</Typography>
111+
</Alert>}
112+
<Grid container spacing={2}>
113+
{catalogItems.map((item) => (
114+
<Grid item xs={12} sm={6} md={4} key={item.name} flex="1 1 0">
115+
<CatalogItemCard
116+
openUrl={() => {
117+
client.host.openExternal(Ref.fromRef(item.ref).toURL(true));
118+
}}
119+
item={item}
120+
canRegister={canRegister}
121+
registered={Object.keys(registryItems).some((i) => i === item.name)}
122+
register={registerCatalogItem}
123+
unregister={unregisterCatalogItem}
124+
/>
125+
</Grid>
126+
))}
127+
<Grid item xs={12} sm={6} md={4} flex="1 1 0">
128+
<Card sx={{ height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
129+
<CardContent>
130+
<IconButton sx={{ height: '100%' }} onClick={() => {
131+
client.host.openExternal('https://vonwig.github.io/prompts.docs/tools/docs/');
132+
}}>
133+
<AddIcon sx={{ width: '100%', height: 100 }} />
134+
</IconButton>
135+
</CardContent>
136+
</Card>
137+
</Grid>
138+
</Grid>
139+
</Stack>
140+
);
141+
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Render turtle svg with badge
2+
3+
import React, { useState } from 'react';
4+
import { Badge, Button, Dialog, DialogContent, DialogTitle, Stack, Typography } from '@mui/material';
5+
import gordon from '../gordon.png';
6+
import { v1 } from '@docker/extension-api-client-types';
7+
import { parse, stringify } from 'yaml';
8+
import { writeFilesToHost } from '../FileWatcher';
9+
10+
const DOCKER_MCP_CONFIG_YML = {
11+
services: {
12+
mcp_docker: {
13+
image: "mcp/docker",
14+
command: ["serve", "--mcp", "--register", "github:docker/labs-ai-tools-for-devs?path=prompts/bootstrap.md"],
15+
volumes: ["/var/run/docker.sock:/var/run/docker.sock", "docker-prompts:/prompts"],
16+
"x-mcp-autoremove": true,
17+
}
18+
},
19+
volumes: {
20+
docker_prompts: {
21+
external: false
22+
}
23+
}
24+
}
25+
26+
const Gordon: React.FC<{ client: v1.DockerDesktopClient }> = ({ client }) => {
27+
return (
28+
<>
29+
<Button sx={{ ml: 7 }} variant="outlined" color="primary" onClick={async () => {
30+
const dialogResult = await client.desktopUI.dialog.showOpenDialog({
31+
title: "Save as Gordon MCP yml",
32+
buttonLabel: "Save",
33+
properties: ["openDirectory", "createDirectory"],
34+
})
35+
if (!dialogResult || dialogResult.canceled) {
36+
return;
37+
}
38+
try {
39+
const path = dialogResult.filePaths[0]
40+
const result = await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/project`, 'alpine:latest', 'ls', '-la', '/project'])
41+
const files = result.stdout.split('\n').filter(line => line.trim() !== '').slice(1).map(line => line.split(' ').filter(Boolean).pop())
42+
const has_gordon_mcp_yml = files.some(file => file?.toLowerCase().endsWith('gordon-mcp.yml'))
43+
if (!has_gordon_mcp_yml) {
44+
await writeFilesToHost(client, [{ path: 'gordon-mcp.yml', content: stringify(DOCKER_MCP_CONFIG_YML) }], [{ source: path, target: '/project' }], '/project')
45+
client.desktopUI.toast.success(`Gordon MCP yml saved to ${path}`)
46+
}
47+
else {
48+
const current_config = await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/project`, 'alpine:latest', 'cat', '/project/gordon-mcp.yml'])
49+
const current_config_yaml = parse(current_config.stdout)
50+
if (current_config_yaml.services.mcp_docker) {
51+
return client.desktopUI.toast.error(`You already have mcp/docker configured in ${path}`)
52+
}
53+
const new_config = {
54+
...current_config_yaml,
55+
services: {
56+
...current_config_yaml.services,
57+
mcp_docker: DOCKER_MCP_CONFIG_YML.services.mcp_docker
58+
},
59+
volumes: {
60+
...current_config_yaml.volumes,
61+
docker_prompts: DOCKER_MCP_CONFIG_YML.volumes.docker_prompts
62+
}
63+
}
64+
await writeFilesToHost(client, [{ path: 'gordon-mcp.yml', content: stringify(new_config) }], [{ source: path, target: '/project' }], '/project')
65+
client.desktopUI.toast.success(`Gordon MCP yml updated in ${path}`)
66+
}
67+
}
68+
catch (e) {
69+
console.error(e)
70+
client.desktopUI.toast.error(`Error saving Gordon MCP yml: ${e}`)
71+
}
72+
73+
}}>
74+
<Stack direction="row" alignItems="center" spacing={1}>
75+
<Typography>Save gordon-mcp.yml</Typography>
76+
<img style={{ height: 30, width: 30 }} src={gordon} alt="Gordon" />
77+
</Stack>
78+
</Button>
79+
</>
80+
);
81+
};
82+
83+
export default Gordon;

src/extension/ui/src/components/PromptCard.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,7 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
5151
</Badge>
5252
</Tooltip>
5353
<Tooltip title="Resources">
54-
5554
<Badge badgeContent={item.resources?.length || "0"} color="secondary">
56-
5755
<AttachFile />
5856
</Badge>
5957
</Tooltip>
@@ -66,8 +64,6 @@ export function CatalogItemCard({ openUrl, item, canRegister, registered, regist
6664
</Stack>
6765
<Button
6866
size="small"
69-
70-
7167
onClick={() => {
7268
trackEvent('registry-changed', { name: item.name, ref: item.ref, action: registered ? 'remove' : 'add' });
7369
setIsRegistering(true)

src/extension/ui/src/gordon.png

306 KB
Loading

0 commit comments

Comments
 (0)