Files
claudia/src/components/MCPImportExport.tsx
2025-06-19 19:24:01 +05:30

369 lines
12 KiB
TypeScript

import React, { useState } from "react";
import { Download, Upload, FileText, Loader2, Info, Network, Settings2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { SelectComponent } from "@/components/ui/select";
import { api } from "@/lib/api";
interface MCPImportExportProps {
/**
* Callback when import is completed
*/
onImportCompleted: (imported: number, failed: number) => void;
/**
* Callback for error messages
*/
onError: (message: string) => void;
}
/**
* Component for importing and exporting MCP server configurations
*/
export const MCPImportExport: React.FC<MCPImportExportProps> = ({
onImportCompleted,
onError,
}) => {
const [importingDesktop, setImportingDesktop] = useState(false);
const [importingJson, setImportingJson] = useState(false);
const [importScope, setImportScope] = useState("local");
/**
* Imports servers from Claude Desktop
*/
const handleImportFromDesktop = async () => {
try {
setImportingDesktop(true);
// Always use "user" scope for Claude Desktop imports (was previously "global")
const result = await api.mcpAddFromClaudeDesktop("user");
// Show detailed results if available
if (result.servers && result.servers.length > 0) {
const successfulServers = result.servers.filter(s => s.success);
const failedServers = result.servers.filter(s => !s.success);
if (successfulServers.length > 0) {
const successMessage = `Successfully imported: ${successfulServers.map(s => s.name).join(", ")}`;
onImportCompleted(result.imported_count, result.failed_count);
// Show success details
if (failedServers.length === 0) {
onError(successMessage);
}
}
if (failedServers.length > 0) {
const failureDetails = failedServers
.map(s => `${s.name}: ${s.error || "Unknown error"}`)
.join("\n");
onError(`Failed to import some servers:\n${failureDetails}`);
}
} else {
onImportCompleted(result.imported_count, result.failed_count);
}
} catch (error: any) {
console.error("Failed to import from Claude Desktop:", error);
onError(error.toString() || "Failed to import from Claude Desktop");
} finally {
setImportingDesktop(false);
}
};
/**
* Handles JSON file import
*/
const handleJsonFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
try {
setImportingJson(true);
const content = await file.text();
// Parse the JSON to validate it
let jsonData;
try {
jsonData = JSON.parse(content);
} catch (e) {
onError("Invalid JSON file. Please check the format.");
return;
}
// Check if it's a single server or multiple servers
if (jsonData.mcpServers) {
// Multiple servers format
let imported = 0;
let failed = 0;
for (const [name, config] of Object.entries(jsonData.mcpServers)) {
try {
const serverConfig = {
type: "stdio",
command: (config as any).command,
args: (config as any).args || [],
env: (config as any).env || {}
};
const result = await api.mcpAddJson(name, JSON.stringify(serverConfig), importScope);
if (result.success) {
imported++;
} else {
failed++;
}
} catch (e) {
failed++;
}
}
onImportCompleted(imported, failed);
} else if (jsonData.type && jsonData.command) {
// Single server format
const name = prompt("Enter a name for this server:");
if (!name) return;
const result = await api.mcpAddJson(name, content, importScope);
if (result.success) {
onImportCompleted(1, 0);
} else {
onError(result.message);
}
} else {
onError("Unrecognized JSON format. Expected MCP server configuration.");
}
} catch (error) {
console.error("Failed to import JSON:", error);
onError("Failed to import JSON file");
} finally {
setImportingJson(false);
// Reset the input
event.target.value = "";
}
};
/**
* Handles exporting servers (placeholder)
*/
const handleExport = () => {
// TODO: Implement export functionality
onError("Export functionality coming soon!");
};
/**
* Starts Claude Code as MCP server
*/
const handleStartMCPServer = async () => {
try {
await api.mcpServe();
onError("Claude Code MCP server started. You can now connect to it from other applications.");
} catch (error) {
console.error("Failed to start MCP server:", error);
onError("Failed to start Claude Code as MCP server");
}
};
return (
<div className="p-6 space-y-6">
<div>
<h3 className="text-base font-semibold">Import & Export</h3>
<p className="text-sm text-muted-foreground mt-1">
Import MCP servers from other sources or export your configuration
</p>
</div>
<div className="space-y-4">
{/* Import Scope Selection */}
<Card className="p-4">
<div className="space-y-3">
<div className="flex items-center gap-2 mb-2">
<Settings2 className="h-4 w-4 text-slate-500" />
<Label className="text-sm font-medium">Import Scope</Label>
</div>
<SelectComponent
value={importScope}
onValueChange={(value: string) => setImportScope(value)}
options={[
{ value: "local", label: "Local (this project only)" },
{ value: "project", label: "Project (shared via .mcp.json)" },
{ value: "user", label: "User (all projects)" },
]}
/>
<p className="text-xs text-muted-foreground">
Choose where to save imported servers from JSON files
</p>
</div>
</Card>
{/* Import from Claude Desktop */}
<Card className="p-4 hover:bg-accent/5 transition-colors">
<div className="space-y-3">
<div className="flex items-start gap-3">
<div className="p-2.5 bg-blue-500/10 rounded-lg">
<Download className="h-5 w-5 text-blue-500" />
</div>
<div className="flex-1">
<h4 className="text-sm font-medium">Import from Claude Desktop</h4>
<p className="text-xs text-muted-foreground mt-1">
Automatically imports all MCP servers from Claude Desktop. Installs to user scope (available across all projects).
</p>
</div>
</div>
<Button
onClick={handleImportFromDesktop}
disabled={importingDesktop}
className="w-full gap-2 bg-primary hover:bg-primary/90"
>
{importingDesktop ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Importing...
</>
) : (
<>
<Download className="h-4 w-4" />
Import from Claude Desktop
</>
)}
</Button>
</div>
</Card>
{/* Import from JSON */}
<Card className="p-4 hover:bg-accent/5 transition-colors">
<div className="space-y-3">
<div className="flex items-start gap-3">
<div className="p-2.5 bg-purple-500/10 rounded-lg">
<FileText className="h-5 w-5 text-purple-500" />
</div>
<div className="flex-1">
<h4 className="text-sm font-medium">Import from JSON</h4>
<p className="text-xs text-muted-foreground mt-1">
Import server configuration from a JSON file
</p>
</div>
</div>
<div>
<input
type="file"
accept=".json"
onChange={handleJsonFileSelect}
disabled={importingJson}
className="hidden"
id="json-file-input"
/>
<Button
onClick={() => document.getElementById("json-file-input")?.click()}
disabled={importingJson}
className="w-full gap-2"
variant="outline"
>
{importingJson ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Importing...
</>
) : (
<>
<FileText className="h-4 w-4" />
Choose JSON File
</>
)}
</Button>
</div>
</div>
</Card>
{/* Export (Coming Soon) */}
<Card className="p-4 opacity-60">
<div className="space-y-3">
<div className="flex items-start gap-3">
<div className="p-2.5 bg-muted rounded-lg">
<Upload className="h-5 w-5 text-muted-foreground" />
</div>
<div className="flex-1">
<h4 className="text-sm font-medium">Export Configuration</h4>
<p className="text-xs text-muted-foreground mt-1">
Export your MCP server configuration
</p>
</div>
</div>
<Button
onClick={handleExport}
disabled={true}
variant="secondary"
className="w-full gap-2"
>
<Upload className="h-4 w-4" />
Export (Coming Soon)
</Button>
</div>
</Card>
{/* Serve as MCP */}
<Card className="p-4 border-primary/20 bg-primary/5 hover:bg-primary/10 transition-colors">
<div className="space-y-3">
<div className="flex items-start gap-3">
<div className="p-2.5 bg-green-500/20 rounded-lg">
<Network className="h-5 w-5 text-green-500" />
</div>
<div className="flex-1">
<h4 className="text-sm font-medium">Use Claude Code as MCP Server</h4>
<p className="text-xs text-muted-foreground mt-1">
Start Claude Code as an MCP server that other applications can connect to
</p>
</div>
</div>
<Button
onClick={handleStartMCPServer}
variant="outline"
className="w-full gap-2 border-green-500/20 hover:bg-green-500/10 hover:text-green-600 hover:border-green-500/50"
>
<Network className="h-4 w-4" />
Start MCP Server
</Button>
</div>
</Card>
</div>
{/* Info Box */}
<Card className="p-4 bg-muted/30">
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm font-medium">
<Info className="h-4 w-4 text-primary" />
<span>JSON Format Examples</span>
</div>
<div className="space-y-3 text-xs">
<div>
<p className="font-medium text-muted-foreground mb-1">Single server:</p>
<pre className="bg-background p-3 rounded-lg overflow-x-auto">
{`{
"type": "stdio",
"command": "/path/to/server",
"args": ["--arg1", "value"],
"env": { "KEY": "value" }
}`}
</pre>
</div>
<div>
<p className="font-medium text-muted-foreground mb-1">Multiple servers (.mcp.json format):</p>
<pre className="bg-background p-3 rounded-lg overflow-x-auto">
{`{
"mcpServers": {
"server1": {
"command": "/path/to/server1",
"args": [],
"env": {}
},
"server2": {
"command": "/path/to/server2",
"args": ["--port", "8080"],
"env": { "API_KEY": "..." }
}
}
}`}
</pre>
</div>
</div>
</div>
</Card>
</div>
);
};