Skip to content

Commit ccbd7f0

Browse files
author
colinmcneil
committed
Support gordon auto connection
1 parent b112ee5 commit ccbd7f0

File tree

8 files changed

+69
-40
lines changed

8 files changed

+69
-40
lines changed

src/extension/ui/src/MCPClients.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@ export type MCPClientState = {
1313
export const getMCPClientStates = async (ddClient: v1.DockerDesktopClient) => {
1414
const mcpClientStates: { [name: string]: MCPClientState } = {};
1515
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-
}
20-
const { path, content } = await mcpClient.readFile(ddClient);
16+
const { path, content } = await mcpClient.readConfig(ddClient);
2117
if (content === null) {
2218
mcpClientStates[mcpClient.name] = { exists: false, configured: false, path, client: mcpClient };
2319
} else if (content === undefined) {

src/extension/ui/src/components/CatalogGrid.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export const CatalogGrid: React.FC<CatalogGridProps> = ({
145145
<Tooltip title="These are environment variables and secrets which you have set for your MCP clients.">
146146
<Tab sx={{ fontSize: '1.5em' }} label="Environment" />
147147
</Tooltip>
148-
<Tooltip title="These are environment variables and secrets which you have set for your MCP clients.">
148+
<Tooltip title="These are clients which you have configured to use your tools.">
149149
<Tab sx={{ fontSize: '1.5em' }} label="Clients" />
150150
</Tooltip>
151151
</Tabs>

src/extension/ui/src/components/tabs/YourClients.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ const MCPClientSettings = ({ mcpClientStates, onUpdate, setButtonsLoading, butto
4141
{iconMap[name as keyof typeof iconMap] && <img src={iconMap[name as keyof typeof iconMap]} alt={name} style={{ width: '2em', height: '2em' }} />}
4242
<Typography variant="h4">{name}</Typography>
4343
{!mcpClientState.exists && <Chip label='No Config Found' color='error' />}
44-
{mcpClientState.exists && mcpClientState.client.name !== 'Gordon' && <Chip label={mcpClientState.configured ? 'Connected' : 'Disconnected'} color={mcpClientState.configured ? 'success' : 'error'} />}
45-
{mcpClientState.exists && mcpClientState.client.name === 'Gordon' && <Chip label='Automatic Connection Not Supported' color='warning' />}
44+
{mcpClientState.exists && <Chip label={mcpClientState.configured ? 'Connected' : 'Disconnected'} color={mcpClientState.configured ? 'success' : 'error'} />}
4645
</Stack>
4746
</AccordionSummary>
4847
<AccordionDetails>
@@ -54,7 +53,7 @@ const MCPClientSettings = ({ mcpClientStates, onUpdate, setButtonsLoading, butto
5453

5554
<Typography sx={{ fontWeight: 'bold' }}>Expected Config Path:</Typography>
5655
<Typography component="pre" sx={{ fontFamily: 'monospace', whiteSpace: 'nowrap', overflow: 'auto', maxWidth: '80%', backgroundColor: 'grey.200', padding: 1, borderRadius: 1, fontSize: '12px' }}>
57-
{mcpClientState.client.expectedConfigPath[client.host.platform as 'win32' | 'darwin' | 'linux']}
56+
{mcpClientState.client.expectedConfigPath?.[client.host.platform as 'win32' | 'darwin' | 'linux'] || 'N/A'}
5857
</Typography>
5958
<Typography sx={{ fontWeight: 'bold' }}>Manually Configure:</Typography>
6059
</Stack>

src/extension/ui/src/context/MCPClientContext.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { POLL_INTERVAL } from '../Constants';
55

66
interface MCPClientContextType {
77
// State
8-
mcpClientStates: { [name: string]: MCPClientState };
8+
mcpClientStates: { [name: string]: MCPClientState } | undefined;
99
buttonsLoading: { [name: string]: boolean };
1010

1111
// Actions
@@ -30,22 +30,28 @@ interface MCPClientProviderProps {
3030

3131
export function MCPClientProvider({ children, client }: MCPClientProviderProps) {
3232
// State
33-
const [mcpClientStates, setMcpClientStates] = useState<{ [name: string]: MCPClientState }>({});
33+
const [mcpClientStates, setMcpClientStates] = useState<{ [name: string]: MCPClientState } | undefined>(undefined);
3434
const [buttonsLoading, setButtonsLoading] = useState<{ [name: string]: boolean }>({});
3535

3636
// Update MCP client states
3737
const updateMCPClientStates = async () => {
38-
const oldStates = mcpClientStates;
38+
const hasExistingState = mcpClientStates !== undefined;
3939
const states = await getMCPClientStates(client)
4040
setMcpClientStates(states);
41+
if (!hasExistingState) {
42+
return
43+
}
44+
const oldStates = { ...mcpClientStates };
45+
46+
console.log('oldStates', oldStates, 'states', states)
4147
// Whenever a client connection changes, show toast to user
42-
const connectedClient = Object.values(states).find(state => state.exists && state.configured);
43-
const disconnectedClient = Object.values(oldStates).find(state => state.exists && !state.configured && states[state.client.name].configured);
44-
if (connectedClient && connectedClient.client.name !== 'Gordon') {
45-
client.desktopUI.toast.success('MCP Client Connected: ' + connectedClient.client.name + '. Restart it to load the Catalog.');
48+
const newlyConnectedClient = Object.values(states).find(state => state.exists && state.configured && !oldStates[state.client.name].configured);
49+
const newlyDisconnectedClient = Object.values(states).find(state => state.exists && !state.configured && oldStates[state.client.name].configured);
50+
if (newlyConnectedClient) {
51+
client.desktopUI.toast.success('Client Connected: ' + newlyConnectedClient.client.name + '. Restart it to load the Catalog.');
4652
}
47-
if (disconnectedClient && disconnectedClient.client.name !== 'Gordon') {
48-
client.desktopUI.toast.error('MCP Client Disconnected: ' + disconnectedClient.client.name + '. Restart it to remove the Catalog.');
53+
if (newlyDisconnectedClient) {
54+
client.desktopUI.toast.error('Client Disconnected: ' + newlyDisconnectedClient.client.name + '. Restart it to remove the Catalog.');
4955
}
5056
}
5157

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class ClaudeDesktopClient implements MCPClient {
2020
linux: '/home/$USER/.config/claude/claude_desktop_config.json',
2121
win32: '%APPDATA%\\Claude\\claude_desktop_config.json'
2222
}
23-
readFile = async (client: v1.DockerDesktopClient) => {
23+
readConfig = async (client: v1.DockerDesktopClient) => {
2424
const platform = client.host.platform
2525
let path = ''
2626
switch (platform) {

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class CursorDesktopClient implements MCPClient {
2121
linux: '$HOME/.cursor/mcp.json',
2222
win32: '$USERPROFILE\\.cursor\\mcp.json'
2323
}
24-
readFile = async (client: v1.DockerDesktopClient) => {
24+
readConfig = async (client: v1.DockerDesktopClient) => {
2525
const platform = client.host.platform as keyof typeof this.expectedConfigPath
2626
const configPath = this.expectedConfigPath[platform].replace('$USER', await getUser(client))
2727
try {
@@ -38,7 +38,7 @@ class CursorDesktopClient implements MCPClient {
3838
}
3939
}
4040
connect = async (client: v1.DockerDesktopClient) => {
41-
const config = await this.readFile(client)
41+
const config = await this.readConfig(client)
4242
let cursorConfig = null
4343
try {
4444
cursorConfig = JSON.parse(config.content || '{}') as typeof SAMPLE_MCP_CONFIG
@@ -69,10 +69,9 @@ class CursorDesktopClient implements MCPClient {
6969
client.desktopUI.toast.error((e as Error).message)
7070
}
7171
}
72-
client.desktopUI.toast.success('Docker MCP Server connected to Cursor')
7372
}
7473
disconnect = async (client: v1.DockerDesktopClient) => {
75-
const config = await this.readFile(client)
74+
const config = await this.readConfig(client)
7675
if (!config.content) {
7776
client.desktopUI.toast.error('No config found')
7877
return

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

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ const DOCKER_MCP_CONFIG_YAML = stringify({
1313
}
1414
})
1515

16-
const gordonConfigPathPlaceholder = 'gordon-mcp.yml in the directory you want to connect to'
16+
type GordonConfig = {
17+
version: string;
18+
features: {
19+
name: string;
20+
enabled: boolean;
21+
}[];
22+
}
1723

1824
class GordonMCPClient implements MCPClient {
1925
name = 'Gordon';
@@ -26,25 +32,48 @@ class GordonMCPClient implements MCPClient {
2632
DOCKER_MCP_CONFIG_YAML +
2733
'</pre>'
2834
]
29-
expectedConfigPath = {
30-
darwin: gordonConfigPathPlaceholder,
31-
linux: gordonConfigPathPlaceholder,
32-
win32: gordonConfigPathPlaceholder
33-
}
34-
readFile = async (client: v1.DockerDesktopClient) => {
35+
readConfig = async (client: v1.DockerDesktopClient) => {
36+
const result = await client.docker.cli.exec('ai', ['config', 'get'])
3537
return {
36-
path: this.expectedConfigPath[client.host.platform as 'darwin' | 'linux' | 'win32'],
37-
content: null
38+
path: 'Docker Desktop AI config',
39+
content: result.stdout
3840
}
3941
}
4042
connect = async (client: v1.DockerDesktopClient) => {
41-
return Promise.resolve();
43+
try {
44+
await client.docker.cli.exec('ai', ['config', 'set-feature', '"MCP Catalog"', 'true'])
45+
} catch (e) {
46+
if ((e as any).stderr) {
47+
client.desktopUI.toast.error((e as any).stderr)
48+
} else {
49+
client.desktopUI.toast.error((e as Error).message)
50+
}
51+
}
4252
}
4353
disconnect = async (client: v1.DockerDesktopClient) => {
44-
return Promise.resolve();
54+
try {
55+
await client.docker.cli.exec('ai', ['config', 'set-feature', '"MCP Catalog"', 'false'])
56+
} catch (e) {
57+
if ((e as any).stderr) {
58+
client.desktopUI.toast.error((e as any).stderr)
59+
} else {
60+
client.desktopUI.toast.error((e as Error).message)
61+
}
62+
}
4563
}
4664
validateConfig = (content: string) => {
47-
return true;
65+
try {
66+
const config = JSON.parse(content) as GordonConfig
67+
return config.features.some(f => f.name === 'MCP Catalog' && f.enabled)
68+
} catch (e) {
69+
if (e instanceof Error) {
70+
console.error(e.message)
71+
}
72+
else {
73+
console.error(JSON.stringify(e))
74+
}
75+
return false
76+
}
4877
}
4978
}
5079

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { v1 } from "@docker/extension-api-client-types";
33
export type MCPClient = {
44
name: string;
55
url: string;
6-
readFile: (client: v1.DockerDesktopClient) => Promise<{ content: string | null | undefined, path: string }>;
7-
connect: (client: v1.DockerDesktopClient) => Promise<void>;
8-
disconnect: (client: v1.DockerDesktopClient) => Promise<void>;
9-
validateConfig: (content: string) => boolean;
10-
expectedConfigPath: { [key in 'win32' | 'darwin' | 'linux']: string };
6+
readConfig: (client: v1.DockerDesktopClient) => Promise<{ content: string | null | undefined, path: string }>; // Reads the config content from the MCP client
7+
connect: (client: v1.DockerDesktopClient) => Promise<void>; // Connects catalog to the MCP client
8+
disconnect: (client: v1.DockerDesktopClient) => Promise<void>; // Disconnects catalog from the MCP client
9+
validateConfig: (content: string) => boolean; // Parses the config content and returns true if it is valid and connected
10+
expectedConfigPath?: { [key in 'win32' | 'darwin' | 'linux']: string }; // Path to the config file, if applicable
1111
manualConfigSteps: string[];
1212
}
1313

0 commit comments

Comments
 (0)