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,285 @@
|
||||
/**
|
||||
* Serviço de validação de protocolo MCP (JSON-RPC)
|
||||
*/
|
||||
|
||||
import { spawn } from "child_process";
|
||||
import type {
|
||||
MCPEnvironment,
|
||||
ValidationResult,
|
||||
} from "@/lib/types/mcp-installer";
|
||||
import { MCP_CONFIG } from "@/lib/config/mcp";
|
||||
|
||||
export class MCPValidatorService {
|
||||
/**
|
||||
* Valida se um servidor MCP está funcionando corretamente
|
||||
* Testa o protocolo JSON-RPC do MCP
|
||||
*/
|
||||
static async validateMCPServer(
|
||||
environment: MCPEnvironment,
|
||||
config: any,
|
||||
timeoutMs: number = MCP_CONFIG.testTimeout
|
||||
): Promise<ValidationResult> {
|
||||
return new Promise((resolve) => {
|
||||
const startTime = Date.now();
|
||||
let hasResolved = false;
|
||||
|
||||
// Preparar comando e argumentos
|
||||
const command = environment.executable;
|
||||
const args = environment.args || [];
|
||||
const env = {
|
||||
...process.env,
|
||||
...environment.env,
|
||||
...(config.env || {}),
|
||||
};
|
||||
|
||||
console.log(`🧪 Testing MCP server: ${command} ${args.join(" ")}`);
|
||||
|
||||
try {
|
||||
// Spawnar processo do servidor MCP
|
||||
const serverProcess = spawn(command, args, {
|
||||
env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const responses: any[] = [];
|
||||
let currentBuffer = "";
|
||||
|
||||
// Timeout
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
serverProcess.kill();
|
||||
resolve({
|
||||
success: false,
|
||||
protocol: "unknown",
|
||||
tools: [],
|
||||
error: `Timeout after ${timeoutMs}ms. Server did not respond.`,
|
||||
});
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
// Capturar stdout (respostas JSON-RPC)
|
||||
serverProcess.stdout?.on("data", (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
stdout += text;
|
||||
currentBuffer += text;
|
||||
|
||||
// Processar respostas JSON-RPC linha por linha
|
||||
const lines = currentBuffer.split("\n");
|
||||
currentBuffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith("{")) {
|
||||
try {
|
||||
const response = JSON.parse(trimmed);
|
||||
responses.push(response);
|
||||
console.log(
|
||||
"📨 MCP Response:",
|
||||
JSON.stringify(response).substring(0, 200)
|
||||
);
|
||||
|
||||
// Verificar se recebemos resposta válida de initialize
|
||||
if (
|
||||
response.id === 1 &&
|
||||
response.result &&
|
||||
response.result.capabilities
|
||||
) {
|
||||
// Protocolo MCP detectado, agora listar ferramentas
|
||||
this.sendRequest(serverProcess, {
|
||||
jsonrpc: "2.0",
|
||||
method: "tools/list",
|
||||
id: 2,
|
||||
});
|
||||
}
|
||||
|
||||
// Verificar se recebemos lista de ferramentas
|
||||
if (response.id === 2 && response.result) {
|
||||
clearTimeout(timeoutId);
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
serverProcess.kill();
|
||||
|
||||
const tools = Array.isArray(response.result.tools)
|
||||
? response.result.tools.map((tool: any) => ({
|
||||
name: tool.name || "unnamed",
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
}))
|
||||
: [];
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
protocol: "mcp",
|
||||
tools,
|
||||
capabilities: responses[0]?.result?.capabilities,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Não é JSON válido, ignorar
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Capturar stderr
|
||||
serverProcess.stderr?.on("data", (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
// Error handler
|
||||
serverProcess.on("error", (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
resolve({
|
||||
success: false,
|
||||
protocol: "unknown",
|
||||
tools: [],
|
||||
error: `Failed to spawn server: ${error.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Exit handler
|
||||
serverProcess.on("exit", (code) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
|
||||
// Se saiu muito rápido sem responder, é um erro
|
||||
const elapsed = Date.now() - startTime;
|
||||
if (elapsed < 1000) {
|
||||
resolve({
|
||||
success: false,
|
||||
protocol: "unknown",
|
||||
tools: [],
|
||||
error: `Server exited quickly with code ${code}. stderr: ${stderr}`,
|
||||
});
|
||||
} else {
|
||||
// Servidor pode ter processado e saído normalmente
|
||||
resolve({
|
||||
success: false,
|
||||
protocol: "unknown",
|
||||
tools: [],
|
||||
error: `Server exited with code ${code} before completing validation`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Aguardar servidor estar pronto (pequeno delay)
|
||||
setTimeout(() => {
|
||||
if (!hasResolved) {
|
||||
// Enviar requisição de initialize
|
||||
this.sendRequest(serverProcess, {
|
||||
jsonrpc: "2.0",
|
||||
method: "initialize",
|
||||
params: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {},
|
||||
clientInfo: {
|
||||
name: "ollahub",
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
id: 1,
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
} catch (error: any) {
|
||||
resolve({
|
||||
success: false,
|
||||
protocol: "unknown",
|
||||
tools: [],
|
||||
error: `Validation error: ${error.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Envia requisição JSON-RPC para o servidor
|
||||
*/
|
||||
private static sendRequest(process: any, request: any): void {
|
||||
try {
|
||||
const requestStr = JSON.stringify(request) + "\n";
|
||||
console.log("📤 Sending MCP request:", requestStr.trim());
|
||||
process.stdin?.write(requestStr);
|
||||
} catch (error: any) {
|
||||
console.error("Failed to send request:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validação simples de servidor (apenas verifica se inicia)
|
||||
* Usado como fallback se validação JSON-RPC falhar
|
||||
*/
|
||||
static async simpleValidation(
|
||||
environment: MCPEnvironment,
|
||||
config: any,
|
||||
timeoutMs: number = 5000
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const command = environment.executable;
|
||||
const args = environment.args || [];
|
||||
const env = {
|
||||
...process.env,
|
||||
...environment.env,
|
||||
...(config.env || {}),
|
||||
};
|
||||
|
||||
try {
|
||||
const serverProcess = spawn(command, args, {
|
||||
env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let hasResolved = false;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
serverProcess.kill();
|
||||
// Se chegou ao timeout sem errar, provavelmente está rodando
|
||||
resolve({ success: true });
|
||||
}
|
||||
}, timeoutMs);
|
||||
|
||||
serverProcess.on("error", (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Failed to start: ${error.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
serverProcess.on("exit", (code) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (!hasResolved) {
|
||||
hasResolved = true;
|
||||
if (code === 0) {
|
||||
resolve({ success: true });
|
||||
} else {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Exited with code ${code}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: `Validation error: ${error.message}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user