feat(mcp): enhance MCP installation with status tracking and custom configs

- Add /mcp-servers/ to .gitignore for MCP server installations
- Extend mcp_installations table with status, logs, and validation fields
- Refactor MCP install route to support custom configs, enable logs, and async installation
- Improve MCPSyncService with promise-based syncing to prevent concurrent operations

This update improves MCP management by adding robust installation tracking, custom configuration support, and better error handling for a more reliable user experience.
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-28 21:59:00 -03:00
1 parent 1042f2136b
commit f19bf10ef3
35 files changed
+4743 -429

No files matched your search

+320
View File
@@ -0,0 +1,320 @@
"use client";
import { useState } from "react";
import { X, Upload, CheckCircle, AlertCircle, Loader2 } from "lucide-react";
interface MCPManualInstallModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess?: (mcpId: string) => void;
}
export function MCPManualInstallModal({
isOpen,
onClose,
onSuccess,
}: MCPManualInstallModalProps) {
const [configJson, setConfigJson] = useState("");
const [mcpId, setMcpId] = useState("");
const [isInstalling, setIsInstalling] = useState(false);
const [error, setError] = useState<string | null>(null);
const [validationError, setValidationError] = useState<string | null>(null);
const [parsedConfig, setParsedConfig] = useState<any>(null);
// Validar JSON enquanto digita
const handleConfigChange = (value: string) => {
setConfigJson(value);
setValidationError(null);
setParsedConfig(null);
if (!value.trim()) {
return;
}
try {
const parsed = JSON.parse(value);
setParsedConfig(parsed);
// Validar estrutura básica
if (parsed.mcpServers) {
const serverNames = Object.keys(parsed.mcpServers);
if (serverNames.length === 0) {
setValidationError("No MCP server found in configuration");
return;
}
const firstServer = parsed.mcpServers[serverNames[0]];
if (!firstServer.command) {
setValidationError("Missing required field: command");
return;
}
// Auto-preencher ID se estiver vazio
if (!mcpId) {
setMcpId(serverNames[0]);
}
} else {
// Formato direto
if (!parsed.command) {
setValidationError("Missing required field: command");
return;
}
}
} catch (err: any) {
setValidationError(`Invalid JSON: ${err.message}`);
}
};
const handleInstall = async () => {
if (!configJson.trim()) {
setError("Configuration is required");
return;
}
if (validationError) {
setError("Please fix validation errors before installing");
return;
}
setIsInstalling(true);
setError(null);
try {
const response = await fetch("/api/mcp/install-manual", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
config: parsedConfig,
mcpId: mcpId || undefined,
enableLogs: true,
}),
});
const data = await response.json();
if (data.success) {
console.log(`✅ Manual installation started for ${data.mcpId}`);
onSuccess?.(data.mcpId);
onClose();
// Reset form
setConfigJson("");
setMcpId("");
setParsedConfig(null);
} else {
setError(data.error || "Failed to start installation");
}
} catch (err: any) {
console.error("Error installing MCP:", err);
setError(err.message || "Failed to start installation");
} finally {
setIsInstalling(false);
}
};
const handlePasteExample = () => {
const example = `{
"mcpServers": {
"time-server": {
"command": "npx",
"args": ["@mcpcentral/mcp-time"]
}
}
}`;
setConfigJson(example);
handleConfigChange(example);
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<Upload className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Install MCP Manually
</h2>
</div>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Content */}
<div className="p-6 space-y-6">
{/* Description */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h3 className="font-medium text-blue-900 dark:text-blue-100 mb-2">
How to use
</h3>
<p className="text-sm text-blue-700 dark:text-blue-300 mb-3">
Paste your MCP server configuration in Claude Desktop format
(claude_desktop_config.json). You can install custom MCPs or MCPs
not available in the marketplace.
</p>
<button
onClick={handlePasteExample}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
Click here to load an example
</button>
</div>
{/* MCP ID (optional) */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
MCP ID (optional)
</label>
<input
type="text"
value={mcpId}
onChange={(e) => setMcpId(e.target.value)}
placeholder="my-custom-mcp (auto-detected if empty)"
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* Configuration JSON */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Configuration JSON *
</label>
<textarea
value={configJson}
onChange={(e) => handleConfigChange(e.target.value)}
placeholder={`Paste your configuration here, e.g.:
{
"mcpServers": {
"time-server": {
"command": "npx",
"args": ["@mcpcentral/mcp-time"]
}
}
}`}
rows={15}
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
/>
{/* Validation Status */}
{configJson.trim() && (
<div className="mt-2 flex items-center gap-2">
{validationError ? (
<>
<AlertCircle className="h-4 w-4 text-red-500" />
<span className="text-sm text-red-600 dark:text-red-400">
{validationError}
</span>
</>
) : parsedConfig ? (
<>
<CheckCircle className="h-4 w-4 text-green-500" />
<span className="text-sm text-green-600 dark:text-green-400">
Valid configuration
</span>
</>
) : null}
</div>
)}
</div>
{/* Preview */}
{parsedConfig && !validationError && (
<div className="bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h3 className="font-medium text-gray-900 dark:text-white mb-3">
Preview
</h3>
<div className="space-y-2 text-sm">
{parsedConfig.mcpServers ? (
Object.entries(parsedConfig.mcpServers).map(
([name, config]: [string, any]) => (
<div key={name} className="space-y-1">
<div className="text-gray-700 dark:text-gray-300">
<span className="font-medium">Server:</span> {name}
</div>
<div className="text-gray-600 dark:text-gray-400">
<span className="font-medium">Command:</span>{" "}
{config.command} {(config.args || []).join(" ")}
</div>
{config.env && (
<div className="text-gray-600 dark:text-gray-400">
<span className="font-medium">Environment:</span>{" "}
{Object.keys(config.env).length} variables
</div>
)}
</div>
)
)
) : (
<div className="space-y-1">
<div className="text-gray-600 dark:text-gray-400">
<span className="font-medium">Command:</span>{" "}
{parsedConfig.command}{" "}
{(parsedConfig.args || []).join(" ")}
</div>
{parsedConfig.env && (
<div className="text-gray-600 dark:text-gray-400">
<span className="font-medium">Environment:</span>{" "}
{Object.keys(parsedConfig.env).length} variables
</div>
)}
</div>
)}
</div>
</div>
)}
{/* Error Message */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<div className="flex items-start gap-2">
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h4 className="font-medium text-red-900 dark:text-red-100">
Installation Error
</h4>
<p className="text-sm text-red-700 dark:text-red-300 mt-1">
{error}
</p>
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="sticky bottom-0 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 p-4 flex gap-3 justify-end">
<button
onClick={onClose}
disabled={isInstalling}
className="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors disabled:opacity-50"
>
Cancel
</button>
<button
onClick={handleInstall}
disabled={isInstalling || !parsedConfig || !!validationError}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{isInstalling ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Installing...
</>
) : (
<>
<Upload className="h-4 w-4" />
Install MCP
</>
)}
</button>
</div>
</div>
</div>
);
}