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:
1 parent
1042f2136b
commit
f19bf10ef3
35 files changed
+4743
-429
No files matched your search
@@ -0,0 +1,348 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPInstallerService } from "@/lib/services/mcp-installer";
|
||||
import { MCPValidatorService } from "@/lib/services/mcp-validator";
|
||||
import {
|
||||
MCPInstallationStatus,
|
||||
type InstallationConfig,
|
||||
type InstallationProgress,
|
||||
type PackageRegistry,
|
||||
} from "@/lib/types/mcp-installer";
|
||||
|
||||
/**
|
||||
* POST /api/mcp/install-manual
|
||||
* Instala um MCP a partir de uma configuração manual (formato Claude Desktop)
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { config, mcpId, enableLogs } = body;
|
||||
|
||||
// Validar estrutura básica
|
||||
if (!config || typeof config !== "object") {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Invalid configuration format" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Extrair primeiro servidor da configuração (se for formato mcpServers)
|
||||
let serverConfig: any;
|
||||
let serverId: string;
|
||||
|
||||
if (config.mcpServers) {
|
||||
// Formato: { "mcpServers": { "server-name": { ... } } }
|
||||
const serverNames = Object.keys(config.mcpServers);
|
||||
if (serverNames.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "No MCP server found in configuration" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
serverId = mcpId || serverNames[0];
|
||||
serverConfig = config.mcpServers[serverNames[0]];
|
||||
} else {
|
||||
// Formato direto: { "command": "...", "args": [...] }
|
||||
serverId = mcpId || "manual-mcp";
|
||||
serverConfig = config;
|
||||
}
|
||||
|
||||
// Validar campos obrigatórios
|
||||
if (!serverConfig.command) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "Missing required field: command" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`🚀 Starting manual installation for: ${serverId}`);
|
||||
console.log(
|
||||
`Command: ${serverConfig.command} ${(serverConfig.args || []).join(" ")}`
|
||||
);
|
||||
|
||||
// Detectar tipo de pacote baseado no comando
|
||||
const { packageRegistry, packageName, repositoryUrl } =
|
||||
detectPackageInfo(serverConfig);
|
||||
|
||||
console.log(`Detected package type: ${packageRegistry} (${packageName})`);
|
||||
|
||||
// Verificar se já está instalado
|
||||
if (MCPRepository.isInstalled(serverId)) {
|
||||
const existing = MCPRepository.getInstalledMCP(serverId);
|
||||
|
||||
if (existing.status === "ready") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `MCP ${serverId} is already installed`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Re-installing MCP ${serverId} (current status: ${existing.status})`
|
||||
);
|
||||
}
|
||||
|
||||
// Preparar configuração de instalação
|
||||
const installConfig: InstallationConfig = {
|
||||
mcpId: serverId,
|
||||
packageName,
|
||||
packageRegistry,
|
||||
repositoryUrl,
|
||||
config: serverConfig,
|
||||
enableLogs: enableLogs === true,
|
||||
};
|
||||
|
||||
// Salvar no banco como PENDING
|
||||
if (!MCPRepository.isInstalled(serverId)) {
|
||||
MCPRepository.saveInstallation(serverId, serverConfig, []);
|
||||
}
|
||||
MCPRepository.updateInstallationStatus(
|
||||
serverId,
|
||||
"pending",
|
||||
"Starting manual installation..."
|
||||
);
|
||||
|
||||
// Iniciar instalação em background
|
||||
installInBackground(installConfig);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Manual installation started",
|
||||
mcpId: serverId,
|
||||
status: "pending",
|
||||
detectedType: packageRegistry,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error starting manual installation:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to start manual installation",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta informações do pacote baseado no comando
|
||||
*/
|
||||
function detectPackageInfo(serverConfig: any): {
|
||||
packageRegistry: PackageRegistry;
|
||||
packageName: string;
|
||||
repositoryUrl?: string;
|
||||
} {
|
||||
const command = serverConfig.command.toLowerCase();
|
||||
const args = serverConfig.args || [];
|
||||
const firstArg = args[0] || "";
|
||||
|
||||
// NPM/NPX
|
||||
if (command === "npx" || command === "npm") {
|
||||
// npx @mcpcentral/mcp-time
|
||||
// npx -y @modelcontextprotocol/server-filesystem
|
||||
const packageArg = args.find(
|
||||
(arg: string) => arg.startsWith("@") || !arg.startsWith("-")
|
||||
);
|
||||
const packageName = packageArg || firstArg || "unknown-npm-package";
|
||||
|
||||
return {
|
||||
packageRegistry: "npm",
|
||||
packageName: packageName.replace(/^-y\s*/, "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// Node
|
||||
if (command === "node" || command.endsWith("node")) {
|
||||
// node dist/index.js -> assume que é local ou já instalado
|
||||
return {
|
||||
packageRegistry: "npm",
|
||||
packageName: "local-node-package",
|
||||
};
|
||||
}
|
||||
|
||||
// Python
|
||||
if (
|
||||
command === "python" ||
|
||||
command === "python3" ||
|
||||
command.includes("python")
|
||||
) {
|
||||
// python -m mcp_server_git
|
||||
// python /path/to/server.py
|
||||
const moduleIndex = args.indexOf("-m");
|
||||
if (moduleIndex !== -1 && args[moduleIndex + 1]) {
|
||||
return {
|
||||
packageRegistry: "pypi",
|
||||
packageName: args[moduleIndex + 1],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
packageRegistry: "pypi",
|
||||
packageName: "local-python-package",
|
||||
};
|
||||
}
|
||||
|
||||
// UV (Python package manager)
|
||||
if (command === "uv" || command === "uvx") {
|
||||
// uvx mcp-server-git
|
||||
const packageName = args[0] || "unknown-uv-package";
|
||||
return {
|
||||
packageRegistry: "pypi",
|
||||
packageName,
|
||||
};
|
||||
}
|
||||
|
||||
// Cargo/Rust
|
||||
if (command === "cargo") {
|
||||
// cargo run --bin mcp-server
|
||||
const binIndex = args.indexOf("--bin");
|
||||
if (binIndex !== -1 && args[binIndex + 1]) {
|
||||
return {
|
||||
packageRegistry: "cargo",
|
||||
packageName: args[binIndex + 1],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
packageRegistry: "cargo",
|
||||
packageName: "local-rust-package",
|
||||
};
|
||||
}
|
||||
|
||||
// Docker
|
||||
if (command === "docker") {
|
||||
return {
|
||||
packageRegistry: "other",
|
||||
packageName: "docker-container",
|
||||
};
|
||||
}
|
||||
|
||||
// Caminho absoluto ou relativo (executável local)
|
||||
if (command.startsWith("/") || command.startsWith("./")) {
|
||||
return {
|
||||
packageRegistry: "other",
|
||||
packageName: "local-executable",
|
||||
};
|
||||
}
|
||||
|
||||
// Padrão: tratar como "other"
|
||||
return {
|
||||
packageRegistry: "other",
|
||||
packageName: command,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa instalação em background (não bloqueia resposta HTTP)
|
||||
*/
|
||||
async function installInBackground(config: InstallationConfig): Promise<void> {
|
||||
const { mcpId, enableLogs } = config;
|
||||
|
||||
try {
|
||||
console.log(`📦 Installing ${mcpId} in background (manual mode)...`);
|
||||
|
||||
// Callback de progresso
|
||||
const onProgress = (progress: InstallationProgress) => {
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
progress.status,
|
||||
progress.message
|
||||
);
|
||||
|
||||
if (enableLogs && progress.logs && progress.logs.length > 0) {
|
||||
MCPRepository.appendLogs(mcpId, progress.logs);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[${mcpId}] ${progress.status}: ${progress.message} (${progress.percentage}%)`
|
||||
);
|
||||
};
|
||||
|
||||
// Instalar
|
||||
onProgress({
|
||||
status: MCPInstallationStatus.DOWNLOADING,
|
||||
message: "Preparing manual installation...",
|
||||
percentage: 5,
|
||||
});
|
||||
|
||||
const environment = await MCPInstallerService.install(config, onProgress);
|
||||
|
||||
// Salvar ambiente
|
||||
MCPRepository.saveEnvironment(mcpId, environment);
|
||||
|
||||
onProgress({
|
||||
status: MCPInstallationStatus.TESTING,
|
||||
message: "Validating MCP server...",
|
||||
percentage: 85,
|
||||
});
|
||||
|
||||
// Validar servidor MCP
|
||||
const validation = await MCPValidatorService.validateMCPServer(
|
||||
environment,
|
||||
config.config
|
||||
);
|
||||
|
||||
// Salvar resultado de validação
|
||||
MCPRepository.saveValidationResult(mcpId, validation);
|
||||
|
||||
if (validation.success) {
|
||||
// Salvar ferramentas descobertas
|
||||
MCPRepository.saveTools(mcpId, validation.tools);
|
||||
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"ready",
|
||||
`Installed successfully with ${validation.tools.length} tools`
|
||||
);
|
||||
|
||||
console.log(
|
||||
`✅ MCP ${mcpId} (manual) installed and validated successfully`
|
||||
);
|
||||
} else {
|
||||
// Validação falhou, mas instalação pode estar OK
|
||||
console.warn(
|
||||
`⚠️ MCP ${mcpId} (manual) installed but validation failed: ${validation.error}`
|
||||
);
|
||||
|
||||
// Tentar validação simples como fallback
|
||||
const simpleValidation = await MCPValidatorService.simpleValidation(
|
||||
environment,
|
||||
config.config
|
||||
);
|
||||
|
||||
if (simpleValidation.success) {
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"ready",
|
||||
"Installed (validation partial)"
|
||||
);
|
||||
console.log(
|
||||
`✅ MCP ${mcpId} (manual) installed (simple validation passed)`
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
validation.error || simpleValidation.error || "Validation failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Manual installation failed for ${mcpId}:`, error);
|
||||
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"failed",
|
||||
error.message || "Manual installation failed"
|
||||
);
|
||||
|
||||
if (enableLogs) {
|
||||
MCPRepository.appendLogs(mcpId, [
|
||||
`ERROR: ${error.message}`,
|
||||
error.stack || "",
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const mcpId = searchParams.get("mcpId");
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const installation = MCPRepository.getInstalledMCP(mcpId);
|
||||
|
||||
if (!installation) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Parsear logs se existirem
|
||||
let logs: string[] = [];
|
||||
if (installation.install_logs) {
|
||||
try {
|
||||
logs = JSON.parse(installation.install_logs);
|
||||
} catch {
|
||||
logs = [installation.install_logs];
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcpId: installation.id,
|
||||
status: installation.status || "unknown",
|
||||
message: installation.status_message || "",
|
||||
logs,
|
||||
environment: installation.environment_path,
|
||||
executableCommand: installation.executable_command,
|
||||
installedAt: installation.installed_at,
|
||||
updatedAt: installation.updated_at,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error getting installation status:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to get installation status",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
+207
-30
@@ -1,11 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPInstallationStatus } from "@/lib/types/mcp";
|
||||
import { DeepNLPService } from "@/lib/services/deepnlp";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPInstallerService } from "@/lib/services/mcp-installer";
|
||||
import { MCPValidatorService } from "@/lib/services/mcp-validator";
|
||||
import { DeepNLPService } from "@/lib/services/deepnlp";
|
||||
import { PulseMCPService } from "@/lib/services/pulsemcp";
|
||||
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
|
||||
import type {
|
||||
InstallationConfig,
|
||||
InstallationProgress,
|
||||
} from "@/lib/types/mcp-installer";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { mcpId } = await request.json();
|
||||
const { mcpId, customConfig, enableLogs } = await request.json();
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
@@ -14,49 +21,219 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Installing MCP: ${mcpId}`);
|
||||
console.log(`🚀 Starting installation for MCP: ${mcpId}`);
|
||||
|
||||
// Verificar se já está instalado
|
||||
if (MCPRepository.isInstalled(mcpId)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `MCP ${mcpId} is already installed`,
|
||||
}, { status: 400 });
|
||||
const existing = MCPRepository.getInstalledMCP(mcpId);
|
||||
|
||||
// Se já está instalado e pronto, retornar erro
|
||||
if (existing.status === "ready") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `MCP ${mcpId} is already installed`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Se está em processo de instalação ou falhou, permitir reinstalação
|
||||
console.log(
|
||||
`Re-installing MCP ${mcpId} (current status: ${existing.status})`
|
||||
);
|
||||
}
|
||||
|
||||
// Obter configuração completa do servidor
|
||||
console.log(`Fetching server config for ${mcpId}`);
|
||||
const serverConfig = await DeepNLPService.getServerConfig(mcpId);
|
||||
|
||||
if (!serverConfig.items || serverConfig.items.length === 0) {
|
||||
throw new Error(`No configuration found for MCP ${mcpId}`);
|
||||
// Obter configuração
|
||||
let serverConfig: any;
|
||||
let packageName: string;
|
||||
let packageRegistry: string | null = null;
|
||||
let repositoryUrl: string | undefined;
|
||||
|
||||
if (customConfig) {
|
||||
// Usar configuração personalizada
|
||||
console.log(`Using custom configuration for ${mcpId}`);
|
||||
serverConfig = customConfig;
|
||||
packageName = customConfig.packageName || mcpId;
|
||||
packageRegistry = customConfig.packageRegistry || null;
|
||||
repositoryUrl = customConfig.repositoryUrl;
|
||||
} else {
|
||||
// Buscar configuração da API
|
||||
console.log(`Fetching configuration for ${mcpId}`);
|
||||
|
||||
try {
|
||||
// Tentar DeepNLP primeiro
|
||||
const deepnlpConfig = await DeepNLPService.getServerConfig(mcpId);
|
||||
if (deepnlpConfig.items && deepnlpConfig.items.length > 0) {
|
||||
serverConfig = deepnlpConfig.items[0];
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`DeepNLP config not found, trying cache...`);
|
||||
}
|
||||
|
||||
// Se não encontrou, tentar cache PulseMCP
|
||||
if (!serverConfig) {
|
||||
const cachedMCP = MCPCacheRepository.findById(mcpId);
|
||||
if (cachedMCP) {
|
||||
serverConfig = PulseMCPService.transformToMCPProvider(cachedMCP);
|
||||
packageName = cachedMCP.package_name || mcpId;
|
||||
packageRegistry = cachedMCP.package_registry;
|
||||
repositoryUrl = cachedMCP.source_code_url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!serverConfig) {
|
||||
throw new Error(`No configuration found for MCP ${mcpId}`);
|
||||
}
|
||||
|
||||
// Extrair informações do pacote
|
||||
packageName = serverConfig.packageName || serverConfig.name || mcpId;
|
||||
}
|
||||
|
||||
// Obter ferramentas disponíveis
|
||||
console.log(`Fetching server tools for ${mcpId}`);
|
||||
const tools = await DeepNLPService.getServerTools(mcpId);
|
||||
|
||||
// Salvar configuração localmente
|
||||
console.log(`Saving MCP ${mcpId} to local database`);
|
||||
MCPRepository.saveInstallation(mcpId, serverConfig.items[0], tools);
|
||||
|
||||
const result: MCPInstallationStatus = {
|
||||
success: true,
|
||||
message: `MCP ${mcpId} installed successfully`,
|
||||
// Preparar configuração de instalação
|
||||
const installConfig: InstallationConfig = {
|
||||
mcpId,
|
||||
packageName,
|
||||
packageRegistry: packageRegistry as any,
|
||||
repositoryUrl,
|
||||
config: serverConfig,
|
||||
enableLogs: enableLogs === true,
|
||||
};
|
||||
|
||||
// Salvar como PENDING no banco
|
||||
if (!MCPRepository.isInstalled(mcpId)) {
|
||||
MCPRepository.saveInstallation(mcpId, serverConfig, []);
|
||||
}
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"pending",
|
||||
"Starting installation..."
|
||||
);
|
||||
|
||||
// Iniciar instalação em background
|
||||
installInBackground(installConfig);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
installation: result,
|
||||
config: serverConfig.items[0],
|
||||
tools: tools,
|
||||
message: "Installation started",
|
||||
mcpId,
|
||||
status: "pending",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error installing MCP:", error);
|
||||
console.error("Error starting installation:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.message || "Failed to install MCP" },
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to start installation",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executa instalação em background (não bloqueia resposta HTTP)
|
||||
*/
|
||||
async function installInBackground(config: InstallationConfig): Promise<void> {
|
||||
const { mcpId, enableLogs } = config;
|
||||
|
||||
try {
|
||||
console.log(`📦 Installing ${mcpId} in background...`);
|
||||
|
||||
// Callback de progresso
|
||||
const onProgress = (progress: InstallationProgress) => {
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
progress.status,
|
||||
progress.message
|
||||
);
|
||||
|
||||
if (enableLogs && progress.logs && progress.logs.length > 0) {
|
||||
MCPRepository.appendLogs(mcpId, progress.logs);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[${mcpId}] ${progress.status}: ${progress.message} (${progress.percentage}%)`
|
||||
);
|
||||
};
|
||||
|
||||
// Instalar
|
||||
onProgress({
|
||||
status: "downloading",
|
||||
message: "Preparing installation...",
|
||||
percentage: 5,
|
||||
});
|
||||
|
||||
const environment = await MCPInstallerService.install(config, onProgress);
|
||||
|
||||
// Salvar ambiente
|
||||
MCPRepository.saveEnvironment(mcpId, environment);
|
||||
|
||||
onProgress({
|
||||
status: "testing",
|
||||
message: "Validating MCP server...",
|
||||
percentage: 85,
|
||||
});
|
||||
|
||||
// Validar servidor MCP
|
||||
const validation = await MCPValidatorService.validateMCPServer(
|
||||
environment,
|
||||
config.config
|
||||
);
|
||||
|
||||
// Salvar resultado de validação
|
||||
MCPRepository.saveValidationResult(mcpId, validation);
|
||||
|
||||
if (validation.success) {
|
||||
// Salvar ferramentas descobertas
|
||||
MCPRepository.saveTools(mcpId, validation.tools);
|
||||
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"ready",
|
||||
`Installed successfully with ${validation.tools.length} tools`
|
||||
);
|
||||
|
||||
console.log(`✅ MCP ${mcpId} installed and validated successfully`);
|
||||
} else {
|
||||
// Validação falhou, mas instalação pode estar OK
|
||||
console.warn(
|
||||
`⚠️ MCP ${mcpId} installed but validation failed: ${validation.error}`
|
||||
);
|
||||
|
||||
// Tentar validação simples como fallback
|
||||
const simpleValidation = await MCPValidatorService.simpleValidation(
|
||||
environment,
|
||||
config.config
|
||||
);
|
||||
|
||||
if (simpleValidation.success) {
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"ready",
|
||||
"Installed (validation partial)"
|
||||
);
|
||||
console.log(`✅ MCP ${mcpId} installed (simple validation passed)`);
|
||||
} else {
|
||||
throw new Error(
|
||||
validation.error || simpleValidation.error || "Validation failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Installation failed for ${mcpId}:`, error);
|
||||
|
||||
MCPRepository.updateInstallationStatus(
|
||||
mcpId,
|
||||
"failed",
|
||||
error.message || "Installation failed"
|
||||
);
|
||||
|
||||
if (enableLogs) {
|
||||
MCPRepository.appendLogs(mcpId, [
|
||||
`ERROR: ${error.message}`,
|
||||
error.stack || "",
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
console.log("Fetching installed MCPs...");
|
||||
|
||||
// Buscar todos os MCPs instalados com detalhes
|
||||
const installedMcps = MCPRepository.listInstalledWithDetails();
|
||||
|
||||
// Enriquecer com dados do cache do marketplace
|
||||
const enrichedMcps = installedMcps.map((mcp) => {
|
||||
// Tentar encontrar no cache usando diferentes critérios
|
||||
const cacheEntry =
|
||||
MCPCacheRepository.findByOwnerRepo(mcp.owner, mcp.repo) ||
|
||||
MCPCacheRepository.findByName(mcp.name) ||
|
||||
MCPCacheRepository.findById(mcp.id);
|
||||
|
||||
if (cacheEntry) {
|
||||
return {
|
||||
...mcp,
|
||||
// Usar dados do cache quando disponíveis
|
||||
author:
|
||||
cacheEntry.publisher_id !== "Unknown"
|
||||
? cacheEntry.publisher_id
|
||||
: mcp.owner,
|
||||
description: cacheEntry.description || `MCP instalado: ${mcp.name}`,
|
||||
repository: cacheEntry.detail_url || cacheEntry.website,
|
||||
homepage: cacheEntry.website,
|
||||
rating: cacheEntry.rating || 0,
|
||||
totalRatings: cacheEntry.review_cnt || 0,
|
||||
category: cacheEntry.category || "other",
|
||||
tags: cacheEntry.content_tag_list
|
||||
? cacheEntry.content_tag_list.split(",")
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...mcp,
|
||||
author: mcp.owner !== "unknown" ? mcp.owner : "Unknown",
|
||||
description: `MCP instalado: ${mcp.name}`,
|
||||
repository: "",
|
||||
homepage: "",
|
||||
rating: 0,
|
||||
totalRatings: 0,
|
||||
category: "other",
|
||||
tags: [],
|
||||
status: mcp.status || "unknown",
|
||||
statusMessage: mcp.status_message || "",
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`Found ${enrichedMcps.length} installed MCPs`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcps: enrichedMcps,
|
||||
total: enrichedMcps.length,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching installed MCPs:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to fetch installed MCPs",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
+30
-18
@@ -58,24 +58,36 @@ export async function GET(request: Request) {
|
||||
console.log(`Found ${cachedMcps.length} MCPs in cache`);
|
||||
|
||||
// Os dados já estão no formato interno após transformação no saveBatch
|
||||
const mcps = cachedMcps.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.content_name,
|
||||
author: item.publisher_id,
|
||||
description: item.description || "",
|
||||
version: "1.0.0",
|
||||
category: item.category as MCPProvider["category"],
|
||||
tags: item.content_tag_list ? item.content_tag_list.split(",") : [],
|
||||
rating: item.rating,
|
||||
totalRatings: item.review_cnt,
|
||||
repository: item.detail_url || "",
|
||||
homepage: item.website || "",
|
||||
installed: false, // Será definido abaixo
|
||||
subfield: item.subfield || "",
|
||||
field: item.field || "",
|
||||
config: [],
|
||||
tools: [],
|
||||
}));
|
||||
const mcps = cachedMcps.map((item) => {
|
||||
// Construir ID no formato owner/repo a partir dos campos do cache
|
||||
// Se owner estiver vazio, usar apenas o repo
|
||||
const correctId =
|
||||
item.owner && item.owner.trim() !== ""
|
||||
? `${item.owner}/${item.repo}`
|
||||
: item.repo;
|
||||
|
||||
return {
|
||||
id: correctId, // Usar owner/repo como ID principal ou apenas repo se owner vazio
|
||||
originalId: item.id, // Manter ID original para referência
|
||||
owner: item.owner || "",
|
||||
repo: item.repo,
|
||||
name: item.content_name,
|
||||
author: item.publisher_id,
|
||||
description: item.description || "",
|
||||
version: "1.0.0",
|
||||
category: item.category as MCPProvider["category"],
|
||||
tags: item.content_tag_list ? item.content_tag_list.split(",") : [],
|
||||
rating: item.rating,
|
||||
totalRatings: item.review_cnt,
|
||||
repository: item.detail_url || "",
|
||||
homepage: item.website || "",
|
||||
installed: false, // Será definido abaixo
|
||||
subfield: item.subfield || "",
|
||||
field: item.field || "",
|
||||
config: [],
|
||||
tools: [],
|
||||
};
|
||||
});
|
||||
|
||||
// Adicionar status de instalação
|
||||
const installedMCPs = MCPRepository.listInstalled();
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { DeepNLPService } from "@/lib/services/deepnlp";
|
||||
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const mcpId = searchParams.get("mcpId");
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Fetching server config for: ${mcpId}`);
|
||||
|
||||
// Primeiro, tentar buscar do cache local (PulseMCP)
|
||||
const [owner, repo] = mcpId.includes("/") ? mcpId.split("/") : ["", mcpId];
|
||||
|
||||
// Buscar no cache usando ID completo ou apenas repo
|
||||
const cachedMcps = MCPCacheRepository.search({
|
||||
query: repo,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (cachedMcps.length > 0) {
|
||||
const cachedMCP = cachedMcps[0];
|
||||
|
||||
// Verificar se tem configuração no ext_info
|
||||
if (cachedMCP.ext_info) {
|
||||
try {
|
||||
const extInfo =
|
||||
typeof cachedMCP.ext_info === "string"
|
||||
? JSON.parse(cachedMCP.ext_info)
|
||||
: cachedMCP.ext_info;
|
||||
|
||||
if (
|
||||
extInfo.config &&
|
||||
Array.isArray(extInfo.config) &&
|
||||
extInfo.config.length > 0
|
||||
) {
|
||||
console.log(`Found config in cache for ${mcpId}`);
|
||||
// Converter formato PulseMCP para formato esperado pelo modal
|
||||
const serverConfig = extInfo.config.find(
|
||||
(c: any) => c.type === "server"
|
||||
);
|
||||
const envConfig = extInfo.config.find((c: any) => c.type === "env");
|
||||
|
||||
if (serverConfig) {
|
||||
// Usar o nome do servidor config como chave
|
||||
const serverName =
|
||||
serverConfig.name || extInfo.package_name || cachedMCP.id;
|
||||
|
||||
const formattedConfig = {
|
||||
mcpServers: {
|
||||
[serverName]: {
|
||||
command: serverConfig.command || "npx",
|
||||
args: serverConfig.args || [],
|
||||
env: envConfig?.variables || {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
config: formattedConfig,
|
||||
source: "pulsemcp",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing ext_info:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Se não encontrou no cache, tentar DeepNLP como fallback
|
||||
try {
|
||||
const serverConfig = await DeepNLPService.getServerConfig(mcpId);
|
||||
|
||||
if (serverConfig.items && serverConfig.items.length > 0) {
|
||||
console.log(`Found config from DeepNLP for ${mcpId}`);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
config: serverConfig.items[0],
|
||||
source: "deepnlp",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`DeepNLP lookup failed for ${mcpId}, continuing...`);
|
||||
}
|
||||
|
||||
// Se não encontrou configuração em nenhum lugar, retornar config vazio
|
||||
console.log(`No config found for ${mcpId}, returning empty config`);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
config: null,
|
||||
source: "none",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching server config:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to fetch server config",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPValidatorService } from "@/lib/services/mcp-validator";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { mcpId } = await request.json();
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID é obrigatório" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`🧪 Testing MCP server: ${mcpId}`);
|
||||
|
||||
// Buscar dados do MCP no banco
|
||||
const mcpData = MCPRepository.getInstalledMCP(mcpId);
|
||||
if (!mcpData) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP não encontrado" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Obter ambiente de instalação
|
||||
const environment = MCPRepository.getEnvironment(mcpId);
|
||||
|
||||
if (!environment) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"MCP não está instalado corretamente. Ambiente não encontrado.",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Testing environment: ${environment.executable} ${environment.args?.join(
|
||||
" "
|
||||
)}`
|
||||
);
|
||||
|
||||
// Validar servidor usando protocolo JSON-RPC MCP
|
||||
const validationResult = await MCPValidatorService.validateMCPServer(
|
||||
environment,
|
||||
mcpData.config
|
||||
);
|
||||
|
||||
// Salvar resultado da validação
|
||||
MCPRepository.saveValidationResult(mcpId, validationResult);
|
||||
|
||||
if (validationResult.success) {
|
||||
// Atualizar ferramentas se validação foi bem-sucedida
|
||||
if (validationResult.tools && validationResult.tools.length > 0) {
|
||||
MCPRepository.saveTools(mcpId, validationResult.tools);
|
||||
}
|
||||
|
||||
console.log(`✅ Test successful for ${mcpId}:`, {
|
||||
protocol: validationResult.protocol,
|
||||
tools: validationResult.tools.length,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
result: {
|
||||
success: true,
|
||||
protocol: validationResult.protocol,
|
||||
toolsCount: validationResult.tools.length,
|
||||
tools: validationResult.tools,
|
||||
capabilities: validationResult.capabilities,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Se validação falhou, tentar validação simples
|
||||
console.warn(`⚠️ JSON-RPC validation failed, trying simple validation...`);
|
||||
|
||||
const simpleResult = await MCPValidatorService.simpleValidation(
|
||||
environment,
|
||||
mcpData.config,
|
||||
5000
|
||||
);
|
||||
|
||||
if (simpleResult.success) {
|
||||
console.log(`✅ Simple validation passed for ${mcpId}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
result: {
|
||||
success: true,
|
||||
protocol: "unknown",
|
||||
message: "Server starts but JSON-RPC validation failed",
|
||||
toolsCount: 0,
|
||||
tools: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Ambas validações falharam
|
||||
console.error(`❌ All validations failed for ${mcpId}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error:
|
||||
validationResult.error || simpleResult.error || "Validation failed",
|
||||
result: {
|
||||
success: false,
|
||||
protocol: validationResult.protocol,
|
||||
error: validationResult.error,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error testing MCP server:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Internal server error",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPInstallationStatus } from "@/lib/types/mcp";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPEnvironmentService } from "@/lib/services/mcp-environment";
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
@@ -13,28 +13,44 @@ export async function DELETE(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Uninstalling MCP: ${mcpId}`);
|
||||
console.log(`🗑️ Uninstalling MCP: ${mcpId}`);
|
||||
|
||||
// Verificar se está instalado
|
||||
if (!MCPRepository.isInstalled(mcpId)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `MCP ${mcpId} is not installed`,
|
||||
}, { status: 400 });
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `MCP ${mcpId} is not installed`,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Obter ambiente antes de remover do banco
|
||||
const environment = MCPRepository.getEnvironment(mcpId);
|
||||
|
||||
// Remover do banco local
|
||||
MCPRepository.removeInstallation(mcpId);
|
||||
|
||||
const result: MCPInstallationStatus = {
|
||||
success: true,
|
||||
message: `MCP ${mcpId} uninstalled successfully`,
|
||||
mcpId,
|
||||
};
|
||||
console.log(`✓ MCP ${mcpId} removed from database`);
|
||||
|
||||
// Limpar arquivos de instalação se ambiente existe
|
||||
if (environment) {
|
||||
try {
|
||||
await MCPEnvironmentService.cleanupEnvironment(environment);
|
||||
console.log(`✓ MCP ${mcpId} environment cleaned up`);
|
||||
} catch (error: any) {
|
||||
console.warn(
|
||||
`⚠️ Could not clean up environment for ${mcpId}: ${error.message}`
|
||||
);
|
||||
// Não falhar a desinstalação se limpeza falhar
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
installation: result,
|
||||
message: `MCP ${mcpId} uninstalled successfully`,
|
||||
mcpId,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error uninstalling MCP:", error);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const { mcpId, newConfig } = await request.json();
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!newConfig) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "New configuration is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Updating config for MCP: ${mcpId}`);
|
||||
|
||||
// Verificar se o MCP está instalado
|
||||
if (!MCPRepository.isInstalled(mcpId)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `MCP ${mcpId} is not installed`,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Atualizar configuração
|
||||
MCPRepository.updateConfig(mcpId, newConfig);
|
||||
|
||||
console.log(`MCP ${mcpId} configuration updated successfully`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `MCP ${mcpId} configuration updated successfully`,
|
||||
mcpId,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error updating MCP configuration:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.message || "Failed to update MCP configuration" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user