Skip to content

Commit ba4810d

Browse files
author
colinmcneil
committed
Add claude connect/disconnect
1 parent e87c6c4 commit ba4810d

File tree

4 files changed

+105
-52
lines changed

4 files changed

+105
-52
lines changed

src/extension/ui/src/App.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { CatalogGrid } from './components/CatalogGrid';
1212
import { MCPClient, POLL_INTERVAL } from './Constants';
1313
import MCPCatalogLogo from './MCP Catalog.svg'
1414
import Settings from './components/Settings';
15-
import { getMCPClientStates } from './MCPClients';
15+
import { getMCPClientStates, MCPClientState } from './MCPClients';
1616

1717
export const client = createDockerDesktopClient();
1818

@@ -26,7 +26,7 @@ export function App() {
2626
const [registryItems, setRegistryItems] = useState<{ [key: string]: { ref: string } }>({});
2727
const [imagesLoadingResults, setImagesLoadingResults] = useState<ExecResult | null>(null);
2828
const [settings, setSettings] = useState<{ showModal: boolean, pollIntervalSeconds: number }>(localStorage.getItem('settings') ? JSON.parse(localStorage.getItem('settings') || '{}') : DEFAULT_SETTINGS);
29-
const [mcpClientStates, setMcpClientStates] = useState<{ [name: string]: { exists: boolean, configured: boolean } }>({});
29+
const [mcpClientStates, setMcpClientStates] = useState<{ [name: string]: MCPClientState }>({});
3030
const loadRegistry = async () => {
3131
setCanRegister(false);
3232
try {
@@ -58,13 +58,18 @@ export function App() {
5858
}
5959
}
6060

61+
const updateMCPClientStates = async () => {
62+
const states = await getMCPClientStates(client)
63+
setMcpClientStates(states);
64+
}
65+
6166
useEffect(() => {
6267
startImagesLoading();
6368
loadRegistry();
64-
getMCPClientStates(client).then(setMcpClientStates);
69+
updateMCPClientStates();
6570
const interval = setInterval(() => {
6671
loadRegistry();
67-
getMCPClientStates(client).then(setMcpClientStates);
72+
updateMCPClientStates();
6873
}, POLL_INTERVAL);
6974
return () => {
7075
clearInterval(interval)
@@ -87,7 +92,7 @@ export function App() {
8792
<Typography variant='h2' sx={{ fontWeight: 'bold', m: 2 }}>Catalog Settings</Typography>
8893
</DialogTitle>
8994
<DialogContent>
90-
<Settings mcpClientStates={mcpClientStates} settings={settings} setSettings={setSettings} />
95+
<Settings onUpdate={updateMCPClientStates} mcpClientStates={mcpClientStates} settings={settings} setSettings={setSettings} />
9196
</DialogContent>
9297
</Dialog>
9398
<Stack direction="column" spacing={1} justifyContent='center' alignItems='center'>

src/extension/ui/src/Constants.ts

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ export type MCPClient = {
1818
name: string;
1919
url: string;
2020
readFile: (client: v1.DockerDesktopClient) => Promise<string | undefined | null>;
21-
writeFile: (client: v1.DockerDesktopClient, content: string) => Promise<void>;
22-
checkConfig: (content: string) => boolean;
21+
connect: (client: v1.DockerDesktopClient) => Promise<void>;
22+
disconnect: (client: v1.DockerDesktopClient) => Promise<void>;
23+
validateConfig: (content: string) => boolean;
2324
}
2425

2526
export const SUPPORTED_MCP_CLIENTS: MCPClient[] = [
@@ -51,27 +52,73 @@ export const SUPPORTED_MCP_CLIENTS: MCPClient[] = [
5152
return null;
5253
}
5354
},
54-
writeFile: async (client: v1.DockerDesktopClient, content: string) => {
55+
connect: async (client: v1.DockerDesktopClient) => {
5556
const platform = client.host.platform
5657
let path = ''
5758
switch (platform) {
5859
case 'darwin':
59-
path = '/Users/$USER/Library/Application Support/Claude Desktop/config.json'
60+
path = '/Users/$USER/Library/Application Support/Claude/'
6061
break;
6162
case 'linux':
62-
path = '/home/$USER/.config/claude/claude_desktop_config.json'
63+
path = '/home/$USER/.config/claude/'
6364
break;
6465
case 'win32':
65-
path = '%APPDATA%\\Claude\\claude_desktop_config.json'
66+
path = '%APPDATA%\\Claude\\'
6667
break;
6768
default:
6869
throw new Error('Unsupported platform: ' + platform)
6970
}
7071
const user = await getUser(client)
7172
path = path.replace('$USER', user)
72-
await client.docker.cli.exec('run', ['--rm', '-v', 'docker-prompts:/docker-prompts', '--workdir', '/docker-prompts', 'vonwig/function_write_files:latest', `'${JSON.stringify({ files: [{ path, content }] })}'`])
73+
let payload = {
74+
mcpServers: {
75+
mcp_docker: DOCKER_MCP_CONFIG
76+
}
77+
}
78+
try {
79+
const result = await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/claude_desktop_config`, 'alpine:latest', 'sh', '-c', `"cat /claude_desktop_config/claude_desktop_config.json"`])
80+
if (result.stdout) {
81+
payload = JSON.parse(result.stdout)
82+
payload.mcpServers.mcp_docker = DOCKER_MCP_CONFIG
83+
}
84+
} catch (e) {
85+
// No config or malformed config found, overwrite it
86+
}
87+
try {
88+
await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/claude_desktop_config`, '--workdir', '/claude_desktop_config', 'vonwig/function_write_files:latest', `'${JSON.stringify({ files: [{ path: 'claude_desktop_config.json', content: JSON.stringify(payload) }] })}'`])
89+
} catch (e) {
90+
client.desktopUI.toast.error((e as any).stderr)
91+
}
92+
},
93+
disconnect: async (client: v1.DockerDesktopClient) => {
94+
const platform = client.host.platform
95+
let path = ''
96+
switch (platform) {
97+
case 'darwin':
98+
path = '/Users/$USER/Library/Application Support/Claude/'
99+
break;
100+
case 'linux':
101+
path = '/home/$USER/.config/claude/'
102+
break;
103+
case 'win32':
104+
path = '%APPDATA%\\Claude\\'
105+
break;
106+
default:
107+
throw new Error('Unsupported platform: ' + platform)
108+
}
109+
const user = await getUser(client)
110+
path = path.replace('$USER', user)
111+
try {
112+
// This method is only called after the config has been validated, so we can safely assume it's a valid config.
113+
const previousConfig = JSON.parse((await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/claude_desktop_config`, '--workdir', '/claude_desktop_config', 'alpine:latest', 'sh', '-c', `"cat /claude_desktop_config/claude_desktop_config.json"`])).stdout || '{}')
114+
const newConfig = { ...previousConfig }
115+
delete newConfig.mcpServers.mcp_docker
116+
await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/claude_desktop_config`, '--workdir', '/claude_desktop_config', 'vonwig/function_write_files:latest', `'${JSON.stringify({ files: [{ path: 'claude_desktop_config.json', content: JSON.stringify(newConfig) }] })}'`])
117+
} catch (e) {
118+
client.desktopUI.toast.error((e as any).stderr)
119+
}
73120
},
74-
checkConfig: (content: string) => {
121+
validateConfig: (content: string) => {
75122
const config = JSON.parse(content)
76123
return Object.keys(config.mcpServers).some(key => key.includes('mcp_docker'))
77124
}

src/extension/ui/src/MCPClients.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
import { v1 } from "@docker/extension-api-client-types";
2-
import { SUPPORTED_MCP_CLIENTS } from "./Constants";
2+
import { MCPClient, SUPPORTED_MCP_CLIENTS } from "./Constants";
33

4-
export type MCPClientState = {
4+
export interface MCPClientState extends MCPClient {
55
exists: boolean;
66
configured: boolean;
7-
error?: string;
87
}
98

109
export const getMCPClientStates = async (ddClient: v1.DockerDesktopClient) => {
1110
const mcpClientStates: { [name: string]: MCPClientState } = {};
1211
for (const mcpClient of SUPPORTED_MCP_CLIENTS) {
1312
const file = await mcpClient.readFile(ddClient);
1413
if (file === null) {
15-
mcpClientStates[mcpClient.name] = { exists: false, configured: false };
14+
mcpClientStates[mcpClient.name] = { exists: false, configured: false, ...mcpClient };
1615
} else if (file === undefined) {
17-
mcpClientStates[mcpClient.name] = { exists: true, configured: false };
16+
mcpClientStates[mcpClient.name] = { exists: true, configured: false, ...mcpClient };
1817
}
1918
else {
20-
mcpClientStates[mcpClient.name] = { exists: true, configured: mcpClient.checkConfig(file) };
19+
mcpClientStates[mcpClient.name] = { exists: true, configured: mcpClient.validateConfig(file), ...mcpClient };
2120
}
2221
}
2322
return mcpClientStates;

src/extension/ui/src/components/Settings.tsx

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,8 @@ import {
44
Typography,
55
Paper,
66
Divider,
7-
Switch,
8-
FormControlLabel,
97
TextField,
108
Button,
11-
Select,
12-
MenuItem,
13-
FormControl,
14-
InputLabel,
15-
Grid,
169
Accordion,
1710
AccordionSummary,
1811
AccordionDetails,
@@ -21,41 +14,32 @@ import {
2114
List,
2215
ListItem,
2316
ListItemText,
24-
ListItemSecondaryAction,
2517
Chip,
26-
Card,
27-
CardContent,
2818
Stack,
2919
Grid2,
30-
ListItemIcon,
31-
ButtonGroup,
32-
Link
20+
Link,
21+
CircularProgress
3322
} from '@mui/material';
3423
import {
3524
ExpandMore as ExpandMoreIcon,
36-
Save as SaveIcon,
37-
Refresh as RefreshIcon,
38-
Info as InfoIcon,
39-
Add as AddIcon,
40-
Delete as DeleteIcon,
41-
Edit as EditIcon,
4225
ContentCopy,
43-
RemoveCircleOutline,
44-
ConnectWithoutContact,
4526
LinkOff,
4627
LinkRounded,
47-
Delete
28+
SaveOutlined
4829
} from '@mui/icons-material';
4930
import { DOCKER_MCP_CONFIG, MCPClient } from '../Constants';
5031
import { client } from '../App';
32+
import { MCPClientState } from '../MCPClients';
5133

52-
const Settings = ({ settings, setSettings, mcpClientStates }: { settings: { showModal: boolean, pollIntervalSeconds: number }, setSettings: (settings: any) => void, mcpClientStates: { [name: string]: { exists: boolean, configured: boolean, error?: string } } }) => {
34+
const Settings = ({ settings, setSettings, mcpClientStates, onUpdate }: { onUpdate: () => Promise<void>, settings: { showModal: boolean, pollIntervalSeconds: number }, setSettings: (settings: any) => void, mcpClientStates: { [name: string]: MCPClientState } }) => {
5335

5436
const updateAndSaveSettings = (settings: any) => {
5537
setSettings(settings);
5638
localStorage.setItem('settings', JSON.stringify(settings));
5739
}
5840

41+
const [buttonsLoading, setButtonsLoading] = useState<{ [name: string]: boolean }>({});
42+
5943
return (
6044
<Stack direction="column" spacing={1} justifyContent='center' alignItems='center'>
6145
{/* MCP Clients Section */}
@@ -70,39 +54,57 @@ const Settings = ({ settings, setSettings, mcpClientStates }: { settings: { show
7054
<AccordionDetails>
7155
<Paper elevation={0} sx={{ p: 2 }}>
7256
<List>
73-
{Object.entries(mcpClientStates).map(([name, state]) => (
57+
{Object.entries(mcpClientStates).map(([name, mcpClientState]) => (
7458
<ListItem key={name} secondaryAction={
7559
<>
76-
{state.exists && state.configured &&
77-
<Button color="warning" variant="outlined" size="small">
60+
{mcpClientState.exists && mcpClientState.configured &&
61+
<Button onClick={async () => {
62+
setButtonsLoading({ ...buttonsLoading, [name]: true });
63+
await mcpClientState.disconnect(client)
64+
await onUpdate();
65+
setButtonsLoading({ ...buttonsLoading, [name]: false });
66+
}} disabled={buttonsLoading[name]} color="warning" variant="outlined" size="small">
7867
<Stack direction="row" alignItems="center" spacing={1}>
7968
<Typography>Disconnect</Typography>
8069
<LinkOff />
70+
{buttonsLoading[name] && <CircularProgress size={16} />}
8171
</Stack>
8272
</Button>
8373
}
84-
{state.exists && !state.configured &&
85-
<Button color="primary" variant="outlined" size="small">
74+
{mcpClientState.exists && !mcpClientState.configured &&
75+
<Button onClick={async () => {
76+
setButtonsLoading({ ...buttonsLoading, [name]: true });
77+
await mcpClientState.connect(client)
78+
await onUpdate();
79+
setButtonsLoading({ ...buttonsLoading, [name]: false });
80+
}} disabled={buttonsLoading[name]} color="primary" variant="outlined" size="small">
8681
<Stack direction="row" alignItems="center" spacing={1}>
8782
<Typography>Connect</Typography>
8883
<LinkRounded />
84+
{buttonsLoading[name] && <CircularProgress size={16} />}
8985
</Stack>
9086
</Button>
9187
}
92-
{!state.exists &&
93-
<Button color="error" variant="outlined" size="small">
88+
{!mcpClientState.exists &&
89+
<Button color="error" variant="outlined" size="small" onClick={async () => {
90+
setButtonsLoading({ ...buttonsLoading, [name]: true });
91+
await mcpClientState.connect(client)
92+
await onUpdate();
93+
setButtonsLoading({ ...buttonsLoading, [name]: false });
94+
}}>
9495
<Stack direction="row" alignItems="center" spacing={1}>
96+
<SaveOutlined />
9597
<Typography>Write Config</Typography>
96-
<SaveIcon />
98+
{buttonsLoading[name] && <CircularProgress size={16} />}
9799
</Stack>
98100
</Button>
99101
}
100102
</>
101103
}>
102104
<ListItemText primary={<Stack direction="row" alignItems="center" spacing={1}>
103105
<Typography variant="h4">{name}</Typography>
104-
{!state.exists && <Chip label='No Config Found' color='error' />}
105-
{state.exists && <Chip label={state.configured ? 'Connected' : 'Disconnected'} color={state.configured ? 'success' : 'error'} />}
106+
{!mcpClientState.exists && <Chip label='No Config Found' color='error' />}
107+
{mcpClientState.exists && <Chip label={mcpClientState.configured ? 'Connected' : 'Disconnected'} color={mcpClientState.configured ? 'success' : 'error'} />}
106108
</Stack>} />
107109
</ListItem>
108110
))}

0 commit comments

Comments
 (0)