1- import React , { useEffect , useState } from 'react' ;
1+ import React , { useEffect , useState , Suspense } from 'react' ;
22import { createDockerDesktopClient } from '@docker/extension-api-client' ;
3- import { Stack , Typography , Button , ButtonGroup , Grid , debounce , Card , CardContent , IconButton , Alert , DialogTitle , Dialog , DialogContent , FormControlLabel , Checkbox , CircularProgress , Paper , DialogActions , Box } from '@mui/material' ;
4- import { CatalogItemWithName } from './components/PromptCard' ;
5- import { getRegistry } from './Registry' ;
6- import { Close , FolderOpenRounded , } from '@mui/icons-material' ;
7- import { ExecResult } from '@docker/extension-api-client-types/dist/v0' ;
3+ import { Stack , Typography , Button , IconButton , Alert , DialogTitle , Dialog , DialogContent , CircularProgress , Paper , Box } from '@mui/material' ;
4+ import { CatalogItemWithName } from './components/tile/Tile' ;
5+ import { Close } from '@mui/icons-material' ;
86import { CatalogGrid } from './components/CatalogGrid' ;
97import { POLL_INTERVAL } from './Constants' ;
108import MCPCatalogLogo from './MCP Catalog.svg'
11- import Settings from './components/Settings' ;
129import { getMCPClientStates , MCPClientState } from './MCPClients' ;
10+ import { CatalogProvider , useCatalogContext } from './context/CatalogContext' ;
11+ import ConfigurationModal from './components/ConfigurationModal' ;
12+
13+ const Settings = React . lazy ( ( ) => import ( './components/Settings' ) ) ;
1314
1415export const client = createDockerDesktopClient ( ) ;
1516
@@ -19,106 +20,147 @@ const DEFAULT_SETTINGS = {
1920}
2021
2122export function App ( ) {
22- const [ canRegister , setCanRegister ] = useState ( false ) ;
23- const [ registryItems , setRegistryItems ] = useState < { [ key : string ] : { ref : string } } > ( { } ) ;
24- const [ imagesLoadingResults , setImagesLoadingResults ] = useState < ExecResult | null > ( null ) ;
2523 const [ settings , setSettings ] = useState < { showModal : boolean , pollIntervalSeconds : number } > ( localStorage . getItem ( 'settings' ) ? JSON . parse ( localStorage . getItem ( 'settings' ) || '{}' ) : DEFAULT_SETTINGS ) ;
2624 const [ mcpClientStates , setMcpClientStates ] = useState < { [ name : string ] : MCPClientState } > ( { } ) ;
27- const loadRegistry = async ( ) => {
28- setCanRegister ( false ) ;
29- try {
30- const result = await getRegistry ( client )
31- setRegistryItems ( result || { } ) ;
32- }
33- catch ( error ) {
34- if ( error instanceof Error ) {
35- client . desktopUI . toast . error ( 'Failed to get prompt registry: ' + error . message ) ;
36- } else {
37- client . desktopUI . toast . error ( 'Failed to get prompt registry: ' + JSON . stringify ( error ) ) ;
38- }
39- }
40- setCanRegister ( true ) ;
41- }
42-
43- const startImagesLoading = async ( ) => {
44- setImagesLoadingResults ( null ) ;
45- try {
46- const result = await client . docker . cli . exec ( 'pull' , [ 'vonwig/function_write_files:latest' ] )
47- await client . docker . cli . exec ( 'pull' , [ 'alpine:latest' ] )
48- await client . docker . cli . exec ( 'pull' , [ 'keinos/sqlite3:latest' ] )
49- setImagesLoadingResults ( result ) ;
50- }
51- catch ( error ) {
52- console . error ( error )
53- if ( error ) {
54- setImagesLoadingResults ( error as ExecResult )
55- }
56- }
57- }
25+ const [ configuringItem , setConfiguringItem ] = useState < CatalogItemWithName | null > ( null ) ;
5826
5927 const updateMCPClientStates = async ( ) => {
6028 const oldStates = mcpClientStates ;
6129 const states = await getMCPClientStates ( client )
6230 setMcpClientStates ( states ) ;
6331 // Whenever a client connection changes, show toast to user
6432 if ( Object . values ( oldStates ) . some ( state => state . exists && ! state . configured ) && Object . values ( states ) . every ( state => state . configured ) ) {
65- client . desktopUI . toast . success ( 'MCP Client Connected. Restart Claude Desktop to use the MCP Catalog.' ) ;
33+ client . desktopUI . toast . success ( 'MCP Client Connected. Restart it to load the Catalog.' ) ;
6634 }
6735 if ( Object . values ( oldStates ) . some ( state => state . exists && state . configured ) && Object . values ( states ) . every ( state => ! state . configured ) ) {
68- client . desktopUI . toast . error ( 'MCP Client Disconnected. Restart Claude Desktop to remove the MCP Catalog.' ) ;
36+ client . desktopUI . toast . error ( 'MCP Client Disconnected. Restart it to remove the Catalog.' ) ;
6937 }
7038 }
7139
7240 useEffect ( ( ) => {
73- startImagesLoading ( ) ;
74- loadRegistry ( ) ;
41+ let interval : NodeJS . Timeout ;
7542 updateMCPClientStates ( ) ;
76- const interval = setInterval ( ( ) => {
77- loadRegistry ( ) ;
43+ interval = setInterval ( ( ) => {
7844 updateMCPClientStates ( ) ;
7945 } , POLL_INTERVAL ) ;
80- return ( ) => {
81- clearInterval ( interval )
82- }
46+ return ( ) => clearInterval ( interval ) ;
8347 } , [ ] ) ;
8448
49+ // Wrap the entire application with our CatalogProvider
50+ return (
51+ < CatalogProvider client = { client } >
52+ < AppContent
53+ settings = { settings }
54+ setSettings = { setSettings }
55+ mcpClientStates = { mcpClientStates }
56+ configuringItem = { configuringItem }
57+ setConfiguringItem = { setConfiguringItem }
58+ updateMCPClientStates = { updateMCPClientStates }
59+ />
60+ </ CatalogProvider >
61+ ) ;
62+ }
63+
64+ interface AppContentProps {
65+ settings : { showModal : boolean , pollIntervalSeconds : number } ;
66+ setSettings : React . Dispatch < React . SetStateAction < { showModal : boolean , pollIntervalSeconds : number } > > ;
67+ mcpClientStates : { [ name : string ] : MCPClientState } ;
68+ configuringItem : CatalogItemWithName | null ;
69+ setConfiguringItem : React . Dispatch < React . SetStateAction < CatalogItemWithName | null > > ;
70+ updateMCPClientStates : ( ) => Promise < void > ;
71+ }
72+
73+ function AppContent ( { settings, setSettings, mcpClientStates, configuringItem, setConfiguringItem, updateMCPClientStates } : AppContentProps ) {
74+ const { imagesLoadingResults, loadImagesIfNeeded, secrets, catalogItems, registryItems, tryLoadSecrets } = useCatalogContext ( ) ;
75+
8576 if ( ! imagesLoadingResults || imagesLoadingResults . stderr ) {
8677 return < Paper sx = { { padding : 2 , height : '90vh' , display : 'flex' , flexDirection : 'column' , justifyContent : 'center' , alignItems : 'center' } } >
8778 { ! imagesLoadingResults && < CircularProgress sx = { { marginBottom : 2 } } /> }
8879 { ! imagesLoadingResults && < Typography > Loading images...</ Typography > }
89- { imagesLoadingResults && < Alert sx = { { fontSize : '1.5em' } } action = { < Button variant = 'outlined' color = 'secondary' onClick = { ( ) => startImagesLoading ( ) } > Retry</ Button > } title = "Error loading images" severity = "error" > { imagesLoadingResults . stderr } </ Alert > }
80+ { imagesLoadingResults && < Alert sx = { { fontSize : '1.5em' } } action = { < Button variant = 'outlined' color = 'secondary' onClick = { ( ) => loadImagesIfNeeded ( ) } > Retry</ Button > } title = "Error loading images" severity = "error" > { imagesLoadingResults . stderr } </ Alert > }
9081 < Typography > { imagesLoadingResults ?. stdout } </ Typography >
9182 </ Paper >
9283 }
9384
94- const hasMCPConfigured = Object . values ( mcpClientStates ) . some ( state => state . exists && state . configured )
85+ if ( ! secrets ) {
86+ return < Paper sx = { { padding : 2 , height : '90vh' , display : 'flex' , flexDirection : 'column' , justifyContent : 'center' , alignItems : 'center' } } >
87+ < CircularProgress />
88+ < Typography > Loading secrets...</ Typography >
89+ </ Paper >
90+ }
91+
92+ if ( ! catalogItems ) {
93+ return < Paper sx = { { padding : 2 , height : '90vh' , display : 'flex' , flexDirection : 'column' , justifyContent : 'center' , alignItems : 'center' } } >
94+ < CircularProgress />
95+ < Typography > Loading catalog...</ Typography >
96+ </ Paper >
97+ }
98+
99+ if ( ! registryItems ) {
100+ return < Paper sx = { { padding : 2 , height : '90vh' , display : 'flex' , flexDirection : 'column' , justifyContent : 'center' , alignItems : 'center' } } >
101+ < CircularProgress />
102+ < Typography > Loading registry...</ Typography >
103+ </ Paper >
104+ }
105+
106+ const hasMCPConfigured = Object . values ( mcpClientStates ) . some ( state => state . exists && state . configured ) ;
95107
96108 return (
97109 < >
98- < Dialog open = { settings . showModal } onClose = { ( ) => setSettings ( { ...settings , showModal : false } ) } fullWidth maxWidth = 'md' >
99- < DialogTitle >
100- < Typography variant = 'h2' sx = { { fontWeight : 'bold' , m : 2 } } > Catalog Settings</ Typography >
101- </ DialogTitle >
102- < DialogContent >
103- < Settings onUpdate = { updateMCPClientStates } mcpClientStates = { mcpClientStates } settings = { settings } setSettings = { setSettings } />
104- </ DialogContent >
105- </ Dialog >
110+ { settings . showModal && (
111+ < Dialog open = { settings . showModal } fullWidth maxWidth = "xl" >
112+ < DialogTitle >
113+ Settings
114+ < IconButton
115+ aria-label = "close"
116+ onClick = { ( ) => setSettings ( { ...settings , showModal : false } ) }
117+ sx = { { position : 'absolute' , right : 8 , top : 8 } }
118+ >
119+ < Close />
120+ </ IconButton >
121+ </ DialogTitle >
122+ < DialogContent >
123+ < Suspense fallback = { < Box sx = { { display : 'flex' , justifyContent : 'center' , p : 4 } } > < CircularProgress /> </ Box > } >
124+ < Settings
125+ settings = { settings }
126+ setSettings = { setSettings }
127+ mcpClientStates = { mcpClientStates }
128+ onUpdate = { updateMCPClientStates }
129+ />
130+ </ Suspense >
131+ </ DialogContent >
132+ </ Dialog >
133+ ) }
134+
135+ { /* Replace the old PromptConfig dialog with our new ConfigurationModal */ }
136+ { configuringItem && (
137+ < ConfigurationModal
138+ open = { configuringItem !== null }
139+ onClose = { ( ) => setConfiguringItem ( null ) }
140+ catalogItem = { configuringItem }
141+ client = { client }
142+ />
143+ ) }
144+
106145 < Stack direction = "column" spacing = { 1 } justifyContent = 'center' alignItems = 'center' >
107146 < img src = { MCPCatalogLogo } alt = "MCP Catalog" height = { 100 } />
108147 { hasMCPConfigured ? < > </ > : < Alert action = { < Button variant = 'outlined' color = 'secondary' onClick = { ( ) => setSettings ( { ...settings , showModal : true } ) } > Configure</ Button > } severity = "error" sx = { { fontWeight : 'bold' } } > MCP Clients are not configured. Please configure MCP Clients to use the MCP Catalog.</ Alert > }
109- < CatalogGrid settingsBadgeProps = { hasMCPConfigured ? { } : {
110- color : hasMCPConfigured ? 'default' : 'error' ,
111- badgeContent : '0 MCP Clients' ,
112- sx : {
113- width : 80 ,
114- height : '100%' ,
115- display : 'flex' ,
116- justifyContent : 'center' ,
117- alignItems : 'center' ,
118- } ,
119- } } showSettings = { ( ) => setSettings ( { ...settings , showModal : true } ) } registryItems = { registryItems } canRegister = { canRegister } client = { client } onRegistryChange = { loadRegistry } />
148+ < CatalogGrid
149+ settingsBadgeProps = { hasMCPConfigured ? { } : {
150+ color : hasMCPConfigured ? 'default' : 'error' ,
151+ badgeContent : '0 MCP Clients' ,
152+ sx : {
153+ width : 80 ,
154+ height : '100%' ,
155+ display : 'flex' ,
156+ justifyContent : 'center' ,
157+ alignItems : 'center' ,
158+ } ,
159+ } }
160+ setConfiguringItem = { setConfiguringItem }
161+ showSettings = { ( ) => setSettings ( { ...settings , showModal : true } ) }
162+ />
120163 </ Stack >
121164 </ >
122- )
123-
165+ ) ;
124166}
0 commit comments