Skip to content

Commit 343731b

Browse files
author
colinmcneil
committed
Refactor MCP client registry
Add Cursor MCP Client Support manually adding configs Greatly reduce runtime engine calls for polling
1 parent 8d1e7dd commit 343731b

File tree

8 files changed

+404
-232
lines changed

8 files changed

+404
-232
lines changed

src/extension/ui/src/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { getRegistry } from './Registry';
66
import { Close, FolderOpenRounded, } from '@mui/icons-material';
77
import { ExecResult } from '@docker/extension-api-client-types/dist/v0';
88
import { CatalogGrid } from './components/CatalogGrid';
9-
import { MCPClient, POLL_INTERVAL } from './Constants';
9+
import { POLL_INTERVAL } from './Constants';
1010
import MCPCatalogLogo from './MCP Catalog.svg'
1111
import Settings from './components/Settings';
1212
import { getMCPClientStates, MCPClientState } from './MCPClients';
@@ -45,6 +45,7 @@ export function App() {
4545
try {
4646
const result = await client.docker.cli.exec('pull', ['vonwig/function_write_files:latest'])
4747
await client.docker.cli.exec('pull', ['alpine:latest'])
48+
await client.docker.cli.exec('pull', ['keinos/sqlite3:latest'])
4849
setImagesLoadingResults(result);
4950
}
5051
catch (error) {
@@ -63,7 +64,7 @@ export function App() {
6364
if (Object.values(oldStates).some(state => state.exists && !state.configured) && Object.values(states).every(state => state.configured)) {
6465
client.desktopUI.toast.success('MCP Client Connected. Restart Claude Desktop to use the MCP Catalog.');
6566
}
66-
if (Object.values(oldStates).every(state => state.configured) && Object.values(states).some(state => !state.configured)) {
67+
if (Object.values(oldStates).some(state => state.exists && state.configured) && Object.values(states).every(state => !state.configured)) {
6768
client.desktopUI.toast.error('MCP Client Disconnected. Restart Claude Desktop to remove the MCP Catalog.');
6869
}
6970
}
@@ -94,7 +95,7 @@ export function App() {
9495

9596
return (
9697
<>
97-
<Dialog open={settings.showModal} onClose={() => setSettings({ ...settings, showModal: false })}>
98+
<Dialog open={settings.showModal} onClose={() => setSettings({ ...settings, showModal: false })} fullWidth maxWidth='md'>
9899
<DialogTitle>
99100
<Typography variant='h2' sx={{ fontWeight: 'bold', m: 2 }}>Catalog Settings</Typography>
100101
</DialogTitle>

src/extension/ui/src/Constants.ts

Lines changed: 3 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,10 @@
1-
import { v1 } from "@docker/extension-api-client-types";
2-
import { getUser, readFileInPromptsVolume } from "./FileWatcher";
3-
41
export const POLL_INTERVAL = 1000 * 30;
52
export const MCP_POLICY_NAME = 'MCP=*';
63
export const DD_BUILD_WITH_SECRET_SUPPORT = 184396;
74
export const CATALOG_URL = 'https://raw.githubusercontent.com/docker/labs-ai-tools-for-devs/refs/heads/main/prompts/catalog.yaml'
8-
export const DOCKER_MCP_CONFIG = {
9-
"command": "docker",
10-
"args": [
11-
"run",
12-
"-i",
13-
"--rm",
14-
"alpine/socat",
15-
"STDIO",
16-
"TCP:host.docker.internal:8811"
17-
]
18-
}
19-
export type MCPClient = {
20-
name: string;
21-
url: string;
22-
readFile: (client: v1.DockerDesktopClient) => Promise<{ content: string | null | undefined, path: string }>;
23-
connect: (client: v1.DockerDesktopClient) => Promise<void>;
24-
disconnect: (client: v1.DockerDesktopClient) => Promise<void>;
25-
validateConfig: (content: string) => boolean;
26-
}
5+
276
export const getUnsupportedSecretMessage = (ddVersion: { version: string, build: number }) => {
287
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.`
298
}
30-
export const SUPPORTED_MCP_CLIENTS: MCPClient[] = [
31-
{
32-
name: 'Claude Desktop',
33-
url: 'https://claude.ai/download',
34-
readFile: async (client: v1.DockerDesktopClient) => {
35-
const platform = client.host.platform
36-
let path = ''
37-
switch (platform) {
38-
case 'darwin':
39-
path = '/Users/$USER/Library/Application Support/Claude/claude_desktop_config.json'
40-
break;
41-
case 'linux':
42-
path = '/home/$USER/.config/claude/claude_desktop_config.json'
43-
break;
44-
case 'win32':
45-
path = '%APPDATA%\\Claude\\claude_desktop_config.json'
46-
break;
47-
default:
48-
throw new Error('Unsupported platform: ' + platform)
49-
}
50-
const user = await getUser(client)
51-
path = path.replace('$USER', user)
52-
try {
53-
const result = await client.docker.cli.exec('run', ['--rm', '--mount', `type=bind,source="${path}",target=/config.json`, 'alpine:latest', 'sh', '-c', `"cat /config.json"`])
54-
return { content: result.stdout || undefined, path: path };
55-
} catch (e) {
56-
return { content: null, path: path };
57-
}
58-
},
59-
connect: async (client: v1.DockerDesktopClient) => {
60-
const platform = client.host.platform
61-
let path = ''
62-
switch (platform) {
63-
case 'darwin':
64-
path = '/Users/$USER/Library/Application Support/Claude/'
65-
break;
66-
case 'linux':
67-
path = '/home/$USER/.config/claude/'
68-
break;
69-
case 'win32':
70-
path = '%APPDATA%\\Claude\\'
71-
break;
72-
default:
73-
throw new Error('Unsupported platform: ' + platform)
74-
}
75-
const user = await getUser(client)
76-
path = path.replace('$USER', user)
77-
let payload = {
78-
mcpServers: {
79-
mcp_docker: DOCKER_MCP_CONFIG
80-
}
81-
}
82-
try {
83-
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"`])
84-
if (result.stdout) {
85-
payload = JSON.parse(result.stdout)
86-
payload.mcpServers.mcp_docker = DOCKER_MCP_CONFIG
87-
}
88-
} catch (e) {
89-
// No config or malformed config found, overwrite it
90-
}
91-
try {
92-
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) }] })}'`])
93-
} catch (e) {
94-
client.desktopUI.toast.error((e as any).stderr)
95-
}
96-
},
97-
disconnect: async (client: v1.DockerDesktopClient) => {
98-
const platform = client.host.platform
99-
let path = ''
100-
switch (platform) {
101-
case 'darwin':
102-
path = '/Users/$USER/Library/Application Support/Claude/'
103-
break;
104-
case 'linux':
105-
path = '/home/$USER/.config/claude/'
106-
break;
107-
case 'win32':
108-
path = '%APPDATA%\\Claude\\'
109-
break;
110-
default:
111-
throw new Error('Unsupported platform: ' + platform)
112-
}
113-
const user = await getUser(client)
114-
path = path.replace('$USER', user)
115-
try {
116-
// This method is only called after the config has been validated, so we can safely assume it's a valid config.
117-
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 || '{}')
118-
const newConfig = { ...previousConfig }
119-
delete newConfig.mcpServers.mcp_docker
120-
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) }] })}'`])
121-
} catch (e) {
122-
client.desktopUI.toast.error((e as any).stderr)
123-
}
124-
},
125-
validateConfig: (content: string) => {
126-
const config = JSON.parse(content)
127-
return Object.keys(config.mcpServers).some(key => key.includes('mcp_docker'))
128-
}
129-
}
130-
]
9+
10+
export const DOCKER_MCP_COMMAND = 'docker run -i --rm alpine/socat STDIO TCP:host.docker.internal:8811'

src/extension/ui/src/MCPClients.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { v1 } from "@docker/extension-api-client-types";
2-
import { MCPClient, SUPPORTED_MCP_CLIENTS } from "./Constants";
2+
import { SUPPORTED_MCP_CLIENTS } from "./mcp-clients";
3+
import { MCPClient } from "./mcp-clients";
34

4-
export interface MCPClientState extends MCPClient {
5+
export type MCPClientState = {
6+
client: MCPClient;
57
exists: boolean;
68
configured: boolean;
79
path: string;
@@ -12,12 +14,12 @@ export const getMCPClientStates = async (ddClient: v1.DockerDesktopClient) => {
1214
for (const mcpClient of SUPPORTED_MCP_CLIENTS) {
1315
const { path, content } = await mcpClient.readFile(ddClient);
1416
if (content === null) {
15-
mcpClientStates[mcpClient.name] = { exists: false, configured: false, path, ...mcpClient };
17+
mcpClientStates[mcpClient.name] = { exists: false, configured: false, path, client: mcpClient };
1618
} else if (content === undefined) {
17-
mcpClientStates[mcpClient.name] = { exists: true, configured: false, path, ...mcpClient };
19+
mcpClientStates[mcpClient.name] = { exists: true, configured: false, path, client: mcpClient };
1820
}
1921
else {
20-
mcpClientStates[mcpClient.name] = { exists: true, configured: mcpClient.validateConfig(content), path: path, ...mcpClient };
22+
mcpClientStates[mcpClient.name] = { exists: true, configured: mcpClient.validateConfig(content), path: path, client: mcpClient };
2123
}
2224
}
2325
return mcpClientStates;

src/extension/ui/src/Secrets.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ namespace Secrets {
3232
console.log('Response', response)
3333
client.desktopUI.toast.success('Secret set successfully')
3434
} catch (error) {
35-
client.desktopUI.toast.error('Failed to set secret: ' + error)
35+
if ((error as any).stderr) {
36+
client.desktopUI.toast.error('Failed to set secret: ' + JSON.stringify(error))
37+
} else {
38+
client.desktopUI.toast.error('Failed to set secret: ' + error)
39+
}
3640
}
3741
}
3842

0 commit comments

Comments
 (0)