Skip to content

Commit bd6a15a

Browse files
authored
Merge pull request #59 from docker/cm/gordon-client
Add Gordon MCP Client
2 parents ee333a6 + 73a8d5e commit bd6a15a

File tree

6 files changed

+161
-99
lines changed

6 files changed

+161
-99
lines changed

src/extension/ui/src/Constants.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export const CATALOG_URL = 'https://raw.githubusercontent.com/docker/labs-ai-too
66
export const getUnsupportedSecretMessage = (ddVersion: { version: string, build: number }) => {
77
return `Secret support is not available in this version of Docker Desktop. You are on version ${ddVersion.version}, but the minimum required version is 4.40.0.`
88
}
9-
10-
export const DOCKER_MCP_COMMAND = 'docker run -i --rm alpine/socat STDIO TCP:host.docker.internal:8811'
9+
export const DOCKER_MCP_IMAGE = 'alpine/socat'
10+
export const DOCKER_MCP_CONTAINER_ARGS = 'STDIO TCP:host.docker.internal:8811'
11+
export const DOCKER_MCP_COMMAND = `docker run -i --rm ${DOCKER_MCP_IMAGE} ${DOCKER_MCP_CONTAINER_ARGS}`

src/extension/ui/src/MCPClients.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ export type MCPClientState = {
77
exists: boolean;
88
configured: boolean;
99
path: string;
10+
preventAutoConnectMessage?: string;
1011
}
1112

1213
export const getMCPClientStates = async (ddClient: v1.DockerDesktopClient) => {
1314
const mcpClientStates: { [name: string]: MCPClientState } = {};
1415
for (const mcpClient of SUPPORTED_MCP_CLIENTS) {
16+
if (mcpClient.name === 'Gordon') {
17+
mcpClientStates[mcpClient.name] = { exists: true, configured: true, path: 'gordon-mcp.yml', client: mcpClient, preventAutoConnectMessage: 'Gordon must be manually connected with a yaml file.' };
18+
continue;
19+
}
1520
const { path, content } = await mcpClient.readFile(ddClient);
1621
if (content === null) {
1722
mcpClientStates[mcpClient.name] = { exists: false, configured: false, path, client: mcpClient };
@@ -21,6 +26,9 @@ export const getMCPClientStates = async (ddClient: v1.DockerDesktopClient) => {
2126
else {
2227
mcpClientStates[mcpClient.name] = { exists: true, configured: mcpClient.validateConfig(content), path: path, client: mcpClient };
2328
}
29+
if (mcpClient.name === 'Cursor') {
30+
mcpClientStates[mcpClient.name].preventAutoConnectMessage = 'Connecting Cursor automatically is not yet supported. Please configure manually in Cursor Settings.';
31+
}
2432
}
2533
return mcpClientStates;
2634
}

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

Lines changed: 93 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -64,105 +64,103 @@ const MCPClientSettings = ({ mcpClientStates, onUpdate, setButtonsLoading, butto
6464
>
6565
<Typography variant="h6">MCP Clients</Typography>
6666
</AccordionSummary>
67-
<AccordionDetails>
68-
<Paper elevation={0} sx={{ p: 2 }}>
69-
<List>
70-
{Object.entries(mcpClientStates).map(([name, mcpClientState]) => (
71-
<ListItem key={name}>
72-
<ListItemText
73-
primary={
67+
<AccordionDetails sx={{ p: 1 }}>
68+
<List sx={{ p: 0 }}>
69+
{Object.entries(mcpClientStates).map(([name, mcpClientState]) => (
70+
<ListItem key={name}>
71+
<ListItemText
72+
primary={
73+
<Stack direction="row" alignItems="center" spacing={1}>
74+
<Typography variant="h4">{name}</Typography>
75+
{!mcpClientState.exists && <Chip label='No Config Found' color='error' />}
76+
{mcpClientState.exists && mcpClientState.client.name !== 'Gordon' && <Chip label={mcpClientState.configured ? 'Connected' : 'Disconnected'} color={mcpClientState.configured ? 'success' : 'error'} />}
77+
{mcpClientState.exists && mcpClientState.client.name === 'Gordon' && <Chip label='Automatic Connection Not Supported' color='warning' />}
78+
<Tooltip title={mcpClientState.preventAutoConnectMessage}>
79+
<span style={{ marginLeft: 'auto' }}>
80+
{mcpClientState.exists && mcpClientState.configured &&
81+
<Button onClick={async () => {
82+
setButtonsLoading({ ...buttonsLoading, [name]: true });
83+
await mcpClientState.client.disconnect(client)
84+
await onUpdate();
85+
setButtonsLoading({ ...buttonsLoading, [name]: false });
86+
}} disabled={buttonsLoading[name] || Boolean(mcpClientState.preventAutoConnectMessage)} color="warning" variant="outlined" size="small">
87+
<Stack direction="row" alignItems="center" spacing={1}>
88+
<Typography>Disconnect</Typography>
89+
<LinkOff />
90+
{buttonsLoading[name] && <CircularProgress size={16} />}
91+
</Stack>
92+
</Button>
93+
}
94+
{mcpClientState.exists && !mcpClientState.configured &&
95+
<Button onClick={async () => {
96+
setButtonsLoading({ ...buttonsLoading, [name]: true });
97+
await mcpClientState.client.connect(client)
98+
await onUpdate();
99+
setButtonsLoading({ ...buttonsLoading, [name]: false });
100+
}} disabled={buttonsLoading[name] || Boolean(mcpClientState.preventAutoConnectMessage)} color="primary" size="small">
101+
<Stack direction="row" alignItems="center" spacing={1}>
102+
<Typography>Connect</Typography>
103+
<LinkRounded />
104+
{buttonsLoading[name] && <CircularProgress size={16} />}
105+
</Stack>
106+
</Button>
107+
}
108+
{!mcpClientState.exists &&
109+
<Button color="error" variant="outlined" size="small" onClick={async () => {
110+
setButtonsLoading({ ...buttonsLoading, [name]: true });
111+
await mcpClientState.client.connect(client)
112+
await onUpdate();
113+
setButtonsLoading({ ...buttonsLoading, [name]: false });
114+
}}>
115+
<Stack direction="row" alignItems="center" spacing={1}>
116+
<SaveOutlined />
117+
<Typography>Write Config</Typography>
118+
{buttonsLoading[name] && <CircularProgress size={16} />}
119+
</Stack>
120+
</Button>
121+
}
122+
</span>
123+
</Tooltip>
124+
</Stack>
125+
}
126+
secondary={
127+
<Stack direction="column" justifyContent="center" spacing={1}>
74128
<Stack direction="row" alignItems="center" spacing={1}>
75-
<Typography variant="h4">{name}</Typography>
76-
{!mcpClientState.exists && <Chip label='No Config Found' color='error' />}
77-
{mcpClientState.exists && <Chip label={mcpClientState.configured ? 'Connected' : 'Disconnected'} color={mcpClientState.configured ? 'success' : 'error'} />}
78-
<Tooltip title={mcpClientState.client.name === 'Cursor' ? 'Connecting Cursor automatically is not yet supported. Please configure manually.' : `You may need to restart ${mcpClientState.client.name} after changing the connection.`}>
79-
<span style={{ marginLeft: 'auto' }}>
80-
{mcpClientState.exists && mcpClientState.configured &&
81-
<Button onClick={async () => {
82-
setButtonsLoading({ ...buttonsLoading, [name]: true });
83-
await mcpClientState.client.disconnect(client)
84-
await onUpdate();
85-
setButtonsLoading({ ...buttonsLoading, [name]: false });
86-
}} disabled={buttonsLoading[name] || mcpClientState.client.name === 'Cursor'} color="warning" variant="outlined" size="small">
87-
<Stack direction="row" alignItems="center" spacing={1}>
88-
<Typography>Disconnect</Typography>
89-
<LinkOff />
90-
{buttonsLoading[name] && <CircularProgress size={16} />}
91-
</Stack>
92-
</Button>
93-
}
94-
{mcpClientState.exists && !mcpClientState.configured &&
95-
<Button onClick={async () => {
96-
setButtonsLoading({ ...buttonsLoading, [name]: true });
97-
await mcpClientState.client.connect(client)
98-
await onUpdate();
99-
setButtonsLoading({ ...buttonsLoading, [name]: false });
100-
}} disabled={buttonsLoading[name] || mcpClientState.client.name === 'Cursor'} color="primary" size="small">
101-
<Stack direction="row" alignItems="center" spacing={1}>
102-
<Typography>Connect</Typography>
103-
<LinkRounded />
104-
{buttonsLoading[name] && <CircularProgress size={16} />}
105-
</Stack>
106-
</Button>
107-
}
108-
{!mcpClientState.exists &&
109-
<Button color="error" variant="outlined" size="small" onClick={async () => {
110-
setButtonsLoading({ ...buttonsLoading, [name]: true });
111-
await mcpClientState.client.connect(client)
112-
await onUpdate();
113-
setButtonsLoading({ ...buttonsLoading, [name]: false });
114-
}}>
115-
<Stack direction="row" alignItems="center" spacing={1}>
116-
<SaveOutlined />
117-
<Typography>Write Config</Typography>
118-
{buttonsLoading[name] && <CircularProgress size={16} />}
119-
</Stack>
120-
</Button>
121-
}
122-
</span>
123-
</Tooltip>
124-
</Stack>
125-
}
126-
secondary={
127-
<Stack direction="column" justifyContent="center" spacing={1}>
128-
<Stack direction="row" alignItems="center" spacing={1}>
129-
<Link href={mcpClientState.client.url} target="_blank" rel="noopener noreferrer" onClick={() => client.host.openExternal(mcpClientState.client.url)}>{mcpClientState.client.url}</Link>
130-
131-
</Stack>
129+
<Link href={mcpClientState.client.url} target="_blank" rel="noopener noreferrer" onClick={() => client.host.openExternal(mcpClientState.client.url)}>{mcpClientState.client.url}</Link>
132130

133-
<Typography sx={{ fontWeight: 'bold' }}>Expected Config Path:</Typography>
134-
<Typography component="pre" sx={{ fontFamily: 'monospace', whiteSpace: 'nowrap', overflow: 'auto', maxWidth: '80%', backgroundColor: 'grey.200', padding: 1, borderRadius: 1, fontSize: '12px' }}>
135-
{mcpClientState.client.expectedConfigPath[client.host.platform as 'win32' | 'darwin' | 'linux']}
136-
</Typography>
137-
<Typography sx={{ fontWeight: 'bold' }}>Manually Configure:</Typography>
138-
<List sx={{ listStyleType: 'decimal', p: 0, pl: 2 }}>
139-
{mcpClientState.client.manualConfigSteps.map((step, index) => (
140-
<ListItem sx={{ display: 'list-item', p: 0 }} key={index}>
141-
<ListItemText primary={<div dangerouslySetInnerHTML={{ __html: step }} />} />
142-
</ListItem>
143-
))}
144-
</List>
145131
</Stack>
146-
}
147-
/>
148-
</ListItem>
149-
))}
150-
</List>
151-
<Divider />
152-
<Alert severity="info">
153-
<AlertTitle>Other MCP Clients</AlertTitle>
154-
You can connect other MCP clients to the same server by specifying the following command:
155-
<Stack direction="row" alignItems="center" justifyContent="space-evenly" spacing={1} sx={{ mt: 2 }}>
156-
<IconButton onClick={() => navigator.clipboard.writeText(DOCKER_MCP_COMMAND)}>
157-
<ContentCopy />
158-
</IconButton>
159-
<Typography variant="caption" sx={theme => ({ backgroundColor: theme.palette.grey[200], padding: 1, borderRadius: 1, fontFamily: 'monospace', whiteSpace: 'nowrap', overflow: 'auto' })}>
160-
{DOCKER_MCP_COMMAND}
161-
</Typography>
162-
</Stack>
163-
</Alert>
164132

165-
</Paper>
133+
<Typography sx={{ fontWeight: 'bold' }}>Expected Config Path:</Typography>
134+
<Typography component="pre" sx={{ fontFamily: 'monospace', whiteSpace: 'nowrap', overflow: 'auto', maxWidth: '80%', backgroundColor: 'grey.200', padding: 1, borderRadius: 1, fontSize: '12px' }}>
135+
{mcpClientState.client.expectedConfigPath[client.host.platform as 'win32' | 'darwin' | 'linux']}
136+
</Typography>
137+
<Typography sx={{ fontWeight: 'bold' }}>Manually Configure:</Typography>
138+
<List sx={{ listStyleType: 'decimal', p: 0, pl: 2 }}>
139+
{mcpClientState.client.manualConfigSteps.map((step, index) => (
140+
<ListItem sx={{ display: 'list-item', p: 0 }} key={index}>
141+
<ListItemText primary={<div dangerouslySetInnerHTML={{ __html: step }} />} />
142+
</ListItem>
143+
))}
144+
</List>
145+
</Stack>
146+
}
147+
/>
148+
</ListItem>
149+
))}
150+
</List>
151+
<Divider />
152+
<Alert severity="info">
153+
<AlertTitle>Other MCP Clients</AlertTitle>
154+
You can connect other MCP clients to the same server by specifying the following command:
155+
<Stack direction="row" alignItems="center" justifyContent="space-evenly" spacing={1} sx={{ mt: 2 }}>
156+
<IconButton onClick={() => navigator.clipboard.writeText(DOCKER_MCP_COMMAND)}>
157+
<ContentCopy />
158+
</IconButton>
159+
<Typography variant="caption" sx={theme => ({ backgroundColor: theme.palette.grey[200], padding: 1, borderRadius: 1, fontFamily: 'monospace', whiteSpace: 'nowrap', overflow: 'auto' })}>
160+
{DOCKER_MCP_COMMAND}
161+
</Typography>
162+
</Stack>
163+
</Alert>
166164
</AccordionDetails>
167165
</Accordion >
168166

src/extension/ui/src/mcp-clients/Cursor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const CURSOR_MCP_CONFIG: CursorMCPConfig['mcpServers'][number] = {
2020

2121
class CursorDesktopClient implements MCPClient {
2222
name = 'Cursor'
23-
url = 'https://www.cursor.com/download'
23+
url = 'https://www.cursor.com/downloads'
2424
manualConfigSteps = [
2525
'Open <strong>Cursor Settings</strong>',
2626
'Click on the <strong>MCP</strong> tab',
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { v1 } from "@docker/extension-api-client-types";
2+
import { MCPClient } from ".";
3+
import { DOCKER_MCP_COMMAND, DOCKER_MCP_CONTAINER_ARGS, DOCKER_MCP_IMAGE } from "../Constants";
4+
import { stringify } from "yaml";
5+
6+
7+
const DOCKER_MCP_CONFIG_YAML = stringify({
8+
services: {
9+
MCP_DOCKER: {
10+
image: DOCKER_MCP_IMAGE,
11+
command: DOCKER_MCP_CONTAINER_ARGS.split(' ')
12+
}
13+
}
14+
})
15+
16+
const gordonConfigPathPlaceholder = 'gordon-mcp.yml in the directory you want to connect to'
17+
18+
class GordonMCPClient implements MCPClient {
19+
name = 'Gordon';
20+
url = 'https://docs.docker.com/desktop/features/gordon/';
21+
manualConfigSteps = [
22+
'Enable Gordon in Docker Desktop',
23+
'Write gordon-mcp.yml to the directory you want to connect to',
24+
'Add MCP_DOCKER to the <code>services</code> section:' +
25+
'<pre style="font-family: monospace; overflow: auto; width: 80%; background-color: grey.200; padding: 1; border-radius: 1; font-size: 12px;">' +
26+
DOCKER_MCP_CONFIG_YAML +
27+
'</pre>'
28+
]
29+
expectedConfigPath = {
30+
darwin: gordonConfigPathPlaceholder,
31+
linux: gordonConfigPathPlaceholder,
32+
win32: gordonConfigPathPlaceholder
33+
}
34+
readFile = async (client: v1.DockerDesktopClient) => {
35+
return {
36+
path: this.expectedConfigPath[client.host.platform as 'darwin' | 'linux' | 'win32'],
37+
content: null
38+
}
39+
}
40+
connect = async (client: v1.DockerDesktopClient) => {
41+
return Promise.resolve();
42+
}
43+
disconnect = async (client: v1.DockerDesktopClient) => {
44+
return Promise.resolve();
45+
}
46+
validateConfig = (content: string) => {
47+
return true;
48+
}
49+
50+
51+
}
52+
53+
export default new GordonMCPClient();

src/extension/ui/src/mcp-clients/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { v1 } from "@docker/extension-api-client-types";
99
import Cursor from "./Cursor";
1010
import ClaudeDesktop from "./ClaudeDesktop";
11+
import Gordon from "./Gordon";
1112

1213
export type MCPClient = {
1314
name: string;
@@ -21,6 +22,7 @@ export type MCPClient = {
2122
}
2223

2324
export const SUPPORTED_MCP_CLIENTS: MCPClient[] = [
25+
Gordon,
2426
ClaudeDesktop,
25-
Cursor
27+
Cursor,
2628
]

0 commit comments

Comments
 (0)