Skip to content

Commit b907575

Browse files
authored
Merge pull request #19 from metatool-ai/improve-json-import-export
Improve JSON import and add support for JSON export
2 parents 2f6ec28 + b3187d0 commit b907575

File tree

1 file changed

+175
-6
lines changed
  • app/(sidebar-layout)/(container)/mcp-servers

1 file changed

+175
-6
lines changed

app/(sidebar-layout)/(container)/mcp-servers/page.tsx

Lines changed: 175 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
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';
1313
import Link from 'next/link';
1414
import { useState } from 'react';
1515
import { 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

Comments
 (0)