99 SortingState ,
1010 useReactTable ,
1111} from '@tanstack/react-table' ;
12- import { Trash2 , Upload } from 'lucide-react' ;
12+ import { Copy , Download , Trash2 , Upload } from 'lucide-react' ;
1313import Link from 'next/link' ;
1414import { useState } from 'react' ;
1515import { useForm } from 'react-hook-form' ;
@@ -60,6 +60,9 @@ export default function MCPServersPage() {
6060 const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
6161 const [ importJson , setImportJson ] = useState ( '' ) ;
6262 const [ importError , setImportError ] = useState ( '' ) ;
63+ const [ exportOpen , setExportOpen ] = useState ( false ) ;
64+ const [ exportJson , setExportJson ] = useState ( '' ) ;
65+ const [ copiedToClipboard , setCopiedToClipboard ] = useState ( false ) ;
6366
6467 const form = useForm ( {
6568 defaultValues : {
@@ -167,11 +170,93 @@ export default function MCPServersPage() {
167170 getFilteredRowModel : getFilteredRowModel ( ) ,
168171 } ) ;
169172
173+ const exportServerConfig = ( ) => {
174+ if ( ! servers . length ) {
175+ toast ( {
176+ title : 'Export Failed' ,
177+ description : 'No MCP servers to export.' ,
178+ variant : 'destructive' ,
179+ } ) ;
180+ return ;
181+ }
182+
183+ // Transform servers array to the required JSON format
184+ const mcpServers = servers . reduce ( ( acc , server ) => {
185+ const serverConfig : any = {
186+ description : server . description || '' ,
187+ type : server . type . toLowerCase ( ) ,
188+ } ;
189+
190+ if ( server . type === McpServerType . STDIO ) {
191+ serverConfig . command = server . command ;
192+ serverConfig . args = server . args || [ ] ;
193+ serverConfig . env = server . env || { } ;
194+ } else if ( server . type === McpServerType . SSE ) {
195+ serverConfig . url = server . url ;
196+ }
197+
198+ acc [ server . name ] = serverConfig ;
199+ return acc ;
200+ } , { } as Record < string , any > ) ;
201+
202+ // Create the final JSON structure
203+ const exportData = {
204+ mcpServers,
205+ } ;
206+
207+ // Convert to JSON string with formatting
208+ const jsonString = JSON . stringify ( exportData , null , 2 ) ;
209+ setExportJson ( jsonString ) ;
210+ setExportOpen ( true ) ;
211+ } ;
212+
213+ const copyToClipboard = ( ) => {
214+ navigator . clipboard . writeText ( exportJson ) . then (
215+ ( ) => {
216+ setCopiedToClipboard ( true ) ;
217+ toast ( {
218+ title : 'Copied to Clipboard' ,
219+ description : 'MCP server configuration copied to clipboard.' ,
220+ variant : 'default' ,
221+ } ) ;
222+ setTimeout ( ( ) => setCopiedToClipboard ( false ) , 2000 ) ;
223+ } ,
224+ ( err ) => {
225+ console . error ( 'Could not copy text: ' , err ) ;
226+ toast ( {
227+ title : 'Copy Failed' ,
228+ description : 'Failed to copy to clipboard.' ,
229+ variant : 'destructive' ,
230+ } ) ;
231+ }
232+ ) ;
233+ } ;
234+
235+ const downloadJson = ( ) => {
236+ // Create and trigger download
237+ const blob = new Blob ( [ exportJson ] , { type : 'application/json' } ) ;
238+ const url = URL . createObjectURL ( blob ) ;
239+ const link = document . createElement ( 'a' ) ;
240+ link . href = url ;
241+ link . download = 'mcp-servers-config.json' ;
242+ document . body . appendChild ( link ) ;
243+ link . click ( ) ;
244+ document . body . removeChild ( link ) ;
245+ URL . revokeObjectURL ( url ) ;
246+
247+ toast ( {
248+ title : 'Download Successful' ,
249+ description : `Downloaded ${ servers . length } MCP server${ servers . length !== 1 ? 's' : '' } configuration.` ,
250+ variant : 'default' ,
251+ } ) ;
252+ } ;
253+
170254 return (
171255 < div >
172256 < div className = 'flex justify-between items-center mb-4' >
173257 < h1 className = 'text-2xl font-bold' > MCP Servers</ h1 >
174258 < div className = 'flex space-x-2' >
259+
175260 < Dialog open = { importOpen } onOpenChange = { setImportOpen } >
176261 < DialogTrigger asChild >
177262 < Button variant = 'outline' >
@@ -183,18 +268,24 @@ export default function MCPServersPage() {
183268 < DialogHeader >
184269 < DialogTitle > Import MCP Servers</ DialogTitle >
185270 < DialogDescription >
186- Import multiple MCP server configurations from JSON. The JSON
271+ Import multiple MCP server configurations from JSON. This will incrementally add MCP servers without overwriting what you have here. The JSON
187272 should follow the format:
188- < pre className = 'mt-2 p-2 bg-gray-100 rounded text-xs overflow-auto' >
273+ < pre className = 'mt-2 p-2 bg-gray-100 rounded text-xs overflow-x- auto whitespace-pre-wrap break-all ' >
189274 { `{
190275 "mcpServers": {
191- "ServerName ": {
276+ "CommandBasedServerName ": {
192277 "command": "command",
193278 "args": ["arg1", "arg2"],
194279 "env": {
195280 "KEY": "value"
196281 },
197- "description": "Optional description"
282+ "description": "Optional description",
283+ "type": "stdio" // optional, defaults to "stdio"
284+ },
285+ "UrlBasedServerName": {
286+ "url": "https://example.com/sse",
287+ "description": "Optional description",
288+ "type": "sse" // optional, defaults to "stdio"
198289 }
199290 }
200291}` }
@@ -256,9 +347,42 @@ export default function MCPServersPage() {
256347 return ;
257348 }
258349
350+ // Process each server based on its type
351+ const processedJson = {
352+ mcpServers : Object . entries ( parsedJson . mcpServers ) . reduce ( ( acc , [ name , serverConfig ] ) => {
353+ const config = serverConfig as any ;
354+ const serverType = config . type ?. toLowerCase ( ) === 'sse'
355+ ? McpServerType . SSE
356+ : McpServerType . STDIO ;
357+
358+ // Create server config based on type
359+ if ( serverType === McpServerType . SSE ) {
360+ acc [ name ] = {
361+ name,
362+ description : config . description || '' ,
363+ url : config . url ,
364+ type : serverType ,
365+ status : McpServerStatus . ACTIVE ,
366+ } ;
367+ } else {
368+ // STDIO type
369+ acc [ name ] = {
370+ name,
371+ description : config . description || '' ,
372+ command : config . command ,
373+ args : config . args || [ ] ,
374+ env : config . env || { } ,
375+ type : serverType ,
376+ status : McpServerStatus . ACTIVE ,
377+ } ;
378+ }
379+ return acc ;
380+ } , { } as Record < string , any > )
381+ } ;
382+
259383 // Import the servers
260384 const result = await bulkImportMcpServers (
261- parsedJson ,
385+ processedJson ,
262386 currentProfile ?. uuid
263387 ) ;
264388
@@ -299,6 +423,51 @@ export default function MCPServersPage() {
299423 </ div >
300424 </ DialogContent >
301425 </ Dialog >
426+ < Button variant = 'outline' onClick = { exportServerConfig } >
427+ < Download className = 'mr-2 h-4 w-4' />
428+ Export JSON
429+ </ Button >
430+ < Dialog open = { exportOpen } onOpenChange = { setExportOpen } >
431+ < DialogContent className = 'sm:max-w-[500px]' >
432+ < DialogHeader >
433+ < DialogTitle > Export MCP Servers JSON</ DialogTitle >
434+ < DialogDescription >
435+ MCP server configurations in JSON format.
436+ </ DialogDescription >
437+ </ DialogHeader >
438+ < div className = 'space-y-4' >
439+ < div className = 'relative' >
440+ < pre className = 'mt-2 p-4 bg-gray-100 rounded text-xs overflow-x-auto whitespace-pre-wrap break-all h-80 overflow-y-auto' >
441+ { exportJson }
442+ </ pre >
443+ < Button
444+ variant = 'outline'
445+ size = 'sm'
446+ className = 'absolute top-2 right-6'
447+ onClick = { copyToClipboard } >
448+ < Copy className = 'h-4 w-4 mr-1' />
449+ { copiedToClipboard ? 'Copied!' : 'Copy' }
450+ </ Button >
451+ </ div >
452+ < div className = 'flex justify-end space-x-2' >
453+ < Button
454+ type = 'button'
455+ variant = 'outline'
456+ onClick = { ( ) => {
457+ setExportOpen ( false ) ;
458+ } } >
459+ Close
460+ </ Button >
461+ < Button
462+ type = 'button'
463+ onClick = { downloadJson } >
464+ < Download className = 'mr-2 h-4 w-4' />
465+ Download
466+ </ Button >
467+ </ div >
468+ </ div >
469+ </ DialogContent >
470+ </ Dialog >
302471 < Dialog open = { open } onOpenChange = { setOpen } >
303472 < DialogTrigger asChild >
304473 < Button > Add MCP Server</ Button >
0 commit comments