1+ import { Icon } from '@iconify/react' ;
2+ import { Dialog } from '@kinvolk/headlamp-plugin/lib/CommonComponents' ;
3+ import Editor from '@monaco-editor/react' ;
4+ import {
5+ Alert ,
6+ Box ,
7+ Button ,
8+ DialogActions ,
9+ DialogContent ,
10+ DialogTitle ,
11+ Paper ,
12+ Tab ,
13+ Tabs ,
14+ Typography ,
15+ } from '@mui/material' ;
16+ import React , { useEffect , useState } from 'react' ;
17+
18+ export interface MCPServer {
19+ name : string ;
20+ command : string ;
21+ args : string [ ] ;
22+ env ?: Record < string , string > ;
23+ enabled : boolean ;
24+ }
25+
26+ export interface MCPConfig {
27+ enabled : boolean ;
28+ servers : MCPServer [ ] ;
29+ }
30+
31+ interface MCPConfigEditorDialogProps {
32+ open : boolean ;
33+ onClose : ( ) => void ;
34+ config : MCPConfig ;
35+ onSave : ( config : MCPConfig ) => void ;
36+ }
37+
38+ export default function MCPConfigEditorDialog ( {
39+ open,
40+ onClose,
41+ config,
42+ onSave,
43+ } : MCPConfigEditorDialogProps ) {
44+ const [ content , setContent ] = useState ( '' ) ;
45+ const [ validationError , setValidationError ] = useState ( '' ) ;
46+ const [ tabValue , setTabValue ] = useState ( 0 ) ;
47+ const themeName = localStorage . getItem ( 'headlampThemePreference' ) ;
48+
49+ useEffect ( ( ) => {
50+ if ( open ) {
51+ setContent ( JSON . stringify ( config , null , 2 ) ) ;
52+ setValidationError ( '' ) ;
53+ setTabValue ( 0 ) ;
54+ }
55+ } , [ config , open ] ) ;
56+
57+ const handleEditorChange = ( value : string | undefined ) => {
58+ if ( value !== undefined ) {
59+ setContent ( value ) ;
60+ setValidationError ( '' ) ;
61+ }
62+ } ;
63+
64+ const validateConfig = ( configToValidate : any ) : string | null => {
65+ if ( typeof configToValidate . enabled !== 'boolean' ) {
66+ return 'enabled field must be a boolean' ;
67+ }
68+
69+ if ( ! Array . isArray ( configToValidate . servers ) ) {
70+ return 'servers field must be an array' ;
71+ }
72+
73+ for ( let i = 0 ; i < configToValidate . servers . length ; i ++ ) {
74+ const server = configToValidate . servers [ i ] ;
75+
76+ if ( typeof server . name !== 'string' || ! server . name . trim ( ) ) {
77+ return `Server ${ i + 1 } : name must be a non-empty string` ;
78+ }
79+
80+ if ( typeof server . command !== 'string' || ! server . command . trim ( ) ) {
81+ return `Server ${ i + 1 } : command must be a non-empty string` ;
82+ }
83+
84+ if ( ! Array . isArray ( server . args ) ) {
85+ return `Server ${ i + 1 } : args must be an array` ;
86+ }
87+
88+ if ( server . env !== undefined ) {
89+ if ( typeof server . env !== 'object' || server . env === null || Array . isArray ( server . env ) ) {
90+ return `Server ${ i + 1 } : env must be an object with string key-value pairs` ;
91+ }
92+
93+ for ( const [ key , value ] of Object . entries ( server . env ) ) {
94+ if ( typeof key !== 'string' || typeof value !== 'string' ) {
95+ return `Server ${ i + 1 } : env must contain only string key-value pairs` ;
96+ }
97+ }
98+ }
99+
100+ if ( typeof server . enabled !== 'boolean' ) {
101+ return `Server ${ i + 1 } : enabled must be a boolean` ;
102+ }
103+ }
104+
105+ return null ;
106+ } ;
107+
108+ const handleSave = ( ) => {
109+ try {
110+ const parsedConfig = JSON . parse ( content ) ;
111+
112+ const error = validateConfig ( parsedConfig ) ;
113+ if ( error ) {
114+ setValidationError ( error ) ;
115+ return ;
116+ }
117+
118+ onSave ( parsedConfig ) ;
119+ onClose ( ) ;
120+ } catch ( error ) {
121+ setValidationError ( error instanceof Error ? error . message : 'Invalid JSON configuration' ) ;
122+ }
123+ } ;
124+
125+ const handleLoadExample = ( ) => {
126+ const exampleConfig : MCPConfig = {
127+ enabled : true ,
128+ servers : [
129+ {
130+ name : "inspektor-gadget" ,
131+ command : "docker" ,
132+ args : [
133+ "mcp" ,
134+ "gateway" ,
135+ "run"
136+ ] ,
137+ enabled : true
138+ } ,
139+ {
140+ name : "flux-mcp" ,
141+ command : "flux-operator-mcp" ,
142+ args : [
143+ "serve"
144+ ] ,
145+ env : {
146+ "KUBECONFIG" : "/Users/ashughildiyal/.kube/config"
147+ } ,
148+ enabled : true
149+ }
150+ ]
151+ } ;
152+
153+ setContent ( JSON . stringify ( exampleConfig , null , 2 ) ) ;
154+ setValidationError ( '' ) ;
155+ } ;
156+
157+ const handleReset = ( ) => {
158+ setContent ( JSON . stringify ( config , null , 2 ) ) ;
159+ setValidationError ( '' ) ;
160+ } ;
161+
162+ const handleTabChange = ( _event : React . SyntheticEvent , newValue : number ) => {
163+ setTabValue ( newValue ) ;
164+ } ;
165+
166+ const getSchemaDocumentation = ( ) => {
167+ return {
168+ enabled : "boolean - Enable/disable all MCP servers" ,
169+ servers : [
170+ {
171+ name : "string - Unique server name" ,
172+ command : "string - Executable command or path" ,
173+ args : [ "array of strings - Command arguments" ] ,
174+ env : {
175+ "KEY" : "string value - Environment variables (optional)"
176+ } ,
177+ enabled : "boolean - Enable/disable this specific server"
178+ }
179+ ]
180+ } ;
181+ } ;
182+
183+ return (
184+ < Dialog
185+ open = { open }
186+ maxWidth = "lg"
187+ fullWidth
188+ withFullScreen
189+ style = { { overflow : 'hidden' } }
190+ onClose = { onClose }
191+ >
192+ < DialogTitle >
193+ < Box display = "flex" justifyContent = "space-between" alignItems = "center" >
194+ < Typography variant = "h6" > Edit MCP Configuration</ Typography >
195+ < Box >
196+ < Button
197+ size = "small"
198+ variant = "text"
199+ onClick = { handleLoadExample }
200+ startIcon = { < Icon icon = "mdi:file-document" /> }
201+ >
202+ Load Example
203+ </ Button >
204+ < Button
205+ size = "small"
206+ variant = "text"
207+ onClick = { handleReset }
208+ startIcon = { < Icon icon = "mdi:refresh" /> }
209+ >
210+ Reset
211+ </ Button >
212+ </ Box >
213+ </ Box >
214+ </ DialogTitle >
215+
216+ < DialogContent >
217+ < Tabs value = { tabValue } onChange = { handleTabChange } sx = { { mb : 2 } } >
218+ < Tab label = "Configuration Editor" />
219+ < Tab label = "Schema Documentation" />
220+ </ Tabs >
221+
222+ { tabValue === 0 && (
223+ < Box height = "100%" >
224+ { validationError && (
225+ < Alert severity = "error" sx = { { mb : 2 } } >
226+ { validationError }
227+ </ Alert >
228+ ) }
229+
230+ < Editor
231+ value = { content }
232+ onChange = { handleEditorChange }
233+ language = "json"
234+ height = "450px"
235+ options = { {
236+ selectOnLineNumbers : true ,
237+ minimap : { enabled : true } ,
238+ formatOnPaste : true ,
239+ formatOnType : true ,
240+ automaticLayout : true ,
241+ wordWrap : 'on' ,
242+ scrollBeyondLastLine : false ,
243+ fontSize : 14 ,
244+ } }
245+ theme = { themeName === 'dark' ? 'vs-dark' : 'light' }
246+ />
247+
248+ < Typography variant = "caption" color = "textSecondary" sx = { { mt : 1 } } >
249+ Edit the JSON configuration above. The editor will automatically format and validate your configuration.
250+ </ Typography >
251+ </ Box >
252+ ) }
253+
254+ { tabValue === 1 && (
255+ < Box >
256+ < Paper sx = { { p : 3 , bgcolor : 'grey.50' } } >
257+ < Typography variant = "h6" gutterBottom >
258+ Configuration Schema
259+ </ Typography >
260+
261+ < Box sx = { { mb : 3 } } >
262+ < Typography variant = "body2" color = "textSecondary" paragraph >
263+ The MCP configuration defines how your AI assistant connects to external tools and services.
264+ Each server represents a separate MCP server that provides specific capabilities.
265+ </ Typography >
266+ </ Box >
267+
268+ < Box sx = { { fontFamily : 'monospace' } } >
269+ < pre style = { {
270+ backgroundColor : themeName === 'dark' ? '#1e1e1e' : '#f5f5f5' ,
271+ padding : '16px' ,
272+ borderRadius : '4px' ,
273+ overflow : 'auto' ,
274+ } } >
275+ { JSON . stringify ( getSchemaDocumentation ( ) , null , 2 ) }
276+ </ pre >
277+ </ Box >
278+
279+ < Box sx = { { mt : 3 } } >
280+ < Typography variant = "subtitle2" gutterBottom >
281+ Field Descriptions:
282+ </ Typography >
283+ < Box component = "ul" sx = { { pl : 2 } } >
284+ < li >
285+ < Typography variant = "body2" >
286+ < strong > enabled</ strong > : Master switch to enable/disable all MCP servers
287+ </ Typography >
288+ </ li >
289+ < li >
290+ < Typography variant = "body2" >
291+ < strong > servers</ strong > : Array of MCP server configurations
292+ </ Typography >
293+ </ li >
294+ < li >
295+ < Typography variant = "body2" >
296+ < strong > name</ strong > : Unique identifier for the server
297+ </ Typography >
298+ </ li >
299+ < li >
300+ < Typography variant = "body2" >
301+ < strong > command</ strong > : The executable to run (e.g., "docker", "npx", "python")
302+ </ Typography >
303+ </ li >
304+ < li >
305+ < Typography variant = "body2" >
306+ < strong > args</ strong > : Command-line arguments passed to the executable
307+ </ Typography >
308+ </ li >
309+ < li >
310+ < Typography variant = "body2" >
311+ < strong > env</ strong > : Optional environment variables for the server process
312+ </ Typography >
313+ </ li >
314+ < li >
315+ < Typography variant = "body2" >
316+ < strong > enabled</ strong > : Toggle individual server on/off without removing configuration
317+ </ Typography >
318+ </ li >
319+ </ Box >
320+ </ Box >
321+ </ Paper >
322+ </ Box >
323+ ) }
324+ </ DialogContent >
325+
326+ < DialogActions >
327+ < Button variant = "outlined" onClick = { onClose } >
328+ Cancel
329+ </ Button >
330+ < Button
331+ variant = "contained"
332+ color = "primary"
333+ onClick = { handleSave }
334+ disabled = { tabValue === 1 }
335+ >
336+ Save Configuration
337+ </ Button >
338+ </ DialogActions >
339+ </ Dialog >
340+ ) ;
341+ }
0 commit comments