- 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.
421 lines
11 KiB
TypeScript
421 lines
11 KiB
TypeScript
import Database from "better-sqlite3";
|
|
import path from "path";
|
|
|
|
const dbPath = path.join(process.cwd(), "database", "ollahub.db");
|
|
const db = new Database(dbPath);
|
|
|
|
export class MCPRepository {
|
|
/**
|
|
* Verificar se um MCP está instalado localmente
|
|
*/
|
|
static isInstalled(mcpId: string): boolean {
|
|
try {
|
|
const stmt = db.prepare("SELECT id FROM mcp_installations WHERE id = ?");
|
|
const result = stmt.get(mcpId);
|
|
return !!result;
|
|
} catch (error) {
|
|
console.error(`Error checking if MCP ${mcpId} is installed:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Salvar instalação de MCP no banco local
|
|
*/
|
|
static saveInstallation(mcpId: string, config: any, tools: any[]): void {
|
|
try {
|
|
// Tentar extrair owner/repo do ID
|
|
let owner: string;
|
|
let repo: string;
|
|
|
|
if (mcpId.includes("/")) {
|
|
const parts = mcpId.split("/");
|
|
owner = parts[0];
|
|
repo = parts.slice(1).join("/"); // Suportar múltiplas barras
|
|
} else {
|
|
// Se não tem /, usar mcpId como repo e extrair owner do config se disponível
|
|
owner = config.owner || config.author || "unknown";
|
|
repo = mcpId;
|
|
}
|
|
|
|
const stmt = db.prepare(`
|
|
INSERT OR REPLACE INTO mcp_installations
|
|
(id, owner, repo, name, config, tools, installed_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
`);
|
|
|
|
stmt.run(
|
|
mcpId,
|
|
owner,
|
|
repo,
|
|
config.name || repo,
|
|
JSON.stringify(config),
|
|
JSON.stringify(tools)
|
|
);
|
|
|
|
console.log(`MCP ${mcpId} saved to local database`);
|
|
} catch (error) {
|
|
console.error(`Error saving MCP ${mcpId} installation:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remover instalação de MCP do banco local
|
|
*/
|
|
static removeInstallation(mcpId: string): void {
|
|
try {
|
|
const stmt = db.prepare("DELETE FROM mcp_installations WHERE id = ?");
|
|
const result = stmt.run(mcpId);
|
|
|
|
if (result.changes > 0) {
|
|
console.log(`MCP ${mcpId} removed from local database`);
|
|
} else {
|
|
console.log(`MCP ${mcpId} was not found in local database`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error removing MCP ${mcpId} installation:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listar todos os MCPs instalados localmente
|
|
*/
|
|
static listInstalled(): string[] {
|
|
try {
|
|
const stmt = db.prepare(
|
|
"SELECT id FROM mcp_installations ORDER BY installed_at DESC"
|
|
);
|
|
const results = stmt.all() as { id: string }[];
|
|
return results.map((row) => row.id);
|
|
} catch (error) {
|
|
console.error("Error listing installed MCPs:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obter configuração local de um MCP
|
|
*/
|
|
static getLocalConfig(mcpId: string): any {
|
|
try {
|
|
const stmt = db.prepare(
|
|
"SELECT config FROM mcp_installations WHERE id = ?"
|
|
);
|
|
const result = stmt.get(mcpId) as { config: string } | undefined;
|
|
|
|
if (result) {
|
|
return JSON.parse(result.config);
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error(`Error getting local config for MCP ${mcpId}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obter ferramentas locais de um MCP
|
|
*/
|
|
static getLocalTools(mcpId: string): any[] {
|
|
try {
|
|
const stmt = db.prepare(
|
|
"SELECT tools FROM mcp_installations WHERE id = ?"
|
|
);
|
|
const result = stmt.get(mcpId) as { tools: string } | undefined;
|
|
|
|
if (result) {
|
|
return JSON.parse(result.tools);
|
|
}
|
|
return [];
|
|
} catch (error) {
|
|
console.error(`Error getting local tools for MCP ${mcpId}:`, error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obter informações completas de um MCP instalado
|
|
*/
|
|
static getInstalledMCP(mcpId: string): any {
|
|
try {
|
|
const stmt = db.prepare(`
|
|
SELECT id, owner, repo, name, config, tools, installed_at, updated_at
|
|
FROM mcp_installations WHERE id = ?
|
|
`);
|
|
const result = stmt.get(mcpId) as any;
|
|
|
|
if (result) {
|
|
return {
|
|
...result,
|
|
config: JSON.parse(result.config),
|
|
tools: JSON.parse(result.tools),
|
|
};
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error(`Error getting installed MCP ${mcpId}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listar todos os MCPs instalados com informações completas
|
|
*/
|
|
static listInstalledWithDetails(): any[] {
|
|
try {
|
|
const stmt = db.prepare(`
|
|
SELECT id, owner, repo, name, config, tools, installed_at, updated_at
|
|
FROM mcp_installations ORDER BY installed_at DESC
|
|
`);
|
|
const results = stmt.all() as any[];
|
|
|
|
return results.map((row) => ({
|
|
...row,
|
|
config: JSON.parse(row.config),
|
|
tools: JSON.parse(row.tools),
|
|
}));
|
|
} catch (error) {
|
|
console.error("Error listing installed MCPs with details:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Atualizar timestamp de um MCP instalado
|
|
*/
|
|
static updateTimestamp(mcpId: string): void {
|
|
try {
|
|
const stmt = db.prepare(
|
|
"UPDATE mcp_installations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?"
|
|
);
|
|
stmt.run(mcpId);
|
|
} catch (error) {
|
|
console.error(`Error updating timestamp for MCP ${mcpId}:`, error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Atualizar configuração de um MCP instalado
|
|
*/
|
|
static updateConfig(mcpId: string, newConfig: any): void {
|
|
try {
|
|
const stmt = db.prepare(`
|
|
UPDATE mcp_installations
|
|
SET config = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`);
|
|
|
|
const result = stmt.run(JSON.stringify(newConfig), mcpId);
|
|
|
|
if (result.changes > 0) {
|
|
console.log(`MCP ${mcpId} configuration updated successfully`);
|
|
} else {
|
|
console.log(`MCP ${mcpId} was not found for configuration update`);
|
|
throw new Error(`MCP ${mcpId} not found`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error updating configuration for MCP ${mcpId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verificar integridade do banco de dados
|
|
*/
|
|
static checkDatabaseIntegrity(): boolean {
|
|
try {
|
|
const result = db.prepare("PRAGMA integrity_check").get() as {
|
|
integrity_check: string;
|
|
};
|
|
return result.integrity_check === "ok";
|
|
} catch (error) {
|
|
console.error("Error checking database integrity:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obter estatísticas das instalações
|
|
*/
|
|
static getInstallationStats(): any {
|
|
try {
|
|
const totalStmt = db.prepare(
|
|
"SELECT COUNT(*) as total FROM mcp_installations"
|
|
);
|
|
const totalResult = totalStmt.get() as { total: number };
|
|
|
|
const recentStmt = db.prepare(`
|
|
SELECT COUNT(*) as recent
|
|
FROM mcp_installations
|
|
WHERE installed_at > datetime('now', '-7 days')
|
|
`);
|
|
const recentResult = recentStmt.get() as { recent: number };
|
|
|
|
const categoriesStmt = db.prepare(`
|
|
SELECT COUNT(DISTINCT owner) as unique_owners
|
|
FROM mcp_installations
|
|
`);
|
|
const categoriesResult = categoriesStmt.get() as {
|
|
unique_owners: number;
|
|
};
|
|
|
|
return {
|
|
total: totalResult.total,
|
|
recent: recentResult.recent,
|
|
uniqueOwners: categoriesResult.unique_owners,
|
|
};
|
|
} catch (error) {
|
|
console.error("Error getting installation stats:", error);
|
|
return { total: 0, recent: 0, uniqueOwners: 0 };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Atualizar status de instalação de um MCP
|
|
*/
|
|
static updateInstallationStatus(
|
|
mcpId: string,
|
|
status: string,
|
|
message?: string
|
|
): void {
|
|
try {
|
|
const stmt = db.prepare(`
|
|
UPDATE mcp_installations
|
|
SET status = ?, status_message = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`);
|
|
|
|
stmt.run(status, message || null, mcpId);
|
|
console.log(`MCP ${mcpId} status updated to: ${status}`);
|
|
} catch (error) {
|
|
console.error(`Error updating status for MCP ${mcpId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Salvar informações de ambiente de um MCP instalado
|
|
*/
|
|
static saveEnvironment(mcpId: string, environment: any): void {
|
|
try {
|
|
const stmt = db.prepare(`
|
|
UPDATE mcp_installations
|
|
SET environment_path = ?,
|
|
executable_command = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`);
|
|
|
|
const executableCommand = `${environment.executable} ${(
|
|
environment.args || []
|
|
).join(" ")}`.trim();
|
|
|
|
stmt.run(environment.path, executableCommand, mcpId);
|
|
console.log(`MCP ${mcpId} environment saved`);
|
|
} catch (error) {
|
|
console.error(`Error saving environment for MCP ${mcpId}:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obter ambiente de um MCP instalado
|
|
*/
|
|
static getEnvironment(mcpId: string): any | null {
|
|
try {
|
|
const stmt = db.prepare(`
|
|
SELECT environment_path, executable_command
|
|
FROM mcp_installations
|
|
WHERE id = ?
|
|
`);
|
|
|
|
const result = stmt.get(mcpId) as any;
|
|
|
|
if (!result || !result.environment_path) {
|
|
return null;
|
|
}
|
|
|
|
// Reconstruir objeto de ambiente
|
|
const parts = result.executable_command.split(" ");
|
|
return {
|
|
path: result.environment_path,
|
|
executable: parts[0],
|
|
args: parts.slice(1),
|
|
};
|
|
} catch (error) {
|
|
console.error(`Error getting environment for MCP ${mcpId}:`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adicionar logs de instalação
|
|
*/
|
|
static appendLogs(mcpId: string, logs: string[]): void {
|
|
try {
|
|
const stmt = db.prepare(`
|
|
SELECT install_logs FROM mcp_installations WHERE id = ?
|
|
`);
|
|
|
|
const result = stmt.get(mcpId) as
|
|
| { install_logs: string | null }
|
|
| undefined;
|
|
const existingLogs = result?.install_logs
|
|
? JSON.parse(result.install_logs)
|
|
: [];
|
|
const updatedLogs = [...existingLogs, ...logs];
|
|
|
|
// Limitar a 1000 linhas de log
|
|
const trimmedLogs = updatedLogs.slice(-1000);
|
|
|
|
const updateStmt = db.prepare(`
|
|
UPDATE mcp_installations
|
|
SET install_logs = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`);
|
|
|
|
updateStmt.run(JSON.stringify(trimmedLogs), mcpId);
|
|
} catch (error) {
|
|
console.error(`Error appending logs for MCP ${mcpId}:`, error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Salvar resultado de validação
|
|
*/
|
|
static saveValidationResult(mcpId: string, validationResult: any): void {
|
|
try {
|
|
const stmt = db.prepare(`
|
|
UPDATE mcp_installations
|
|
SET validation_result = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`);
|
|
|
|
stmt.run(JSON.stringify(validationResult), mcpId);
|
|
console.log(`MCP ${mcpId} validation result saved`);
|
|
} catch (error) {
|
|
console.error(`Error saving validation result for MCP ${mcpId}:`, error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Salvar ferramentas validadas
|
|
*/
|
|
static saveTools(mcpId: string, tools: any[]): void {
|
|
try {
|
|
const stmt = db.prepare(`
|
|
UPDATE mcp_installations
|
|
SET tools = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`);
|
|
|
|
stmt.run(JSON.stringify(tools), mcpId);
|
|
console.log(`MCP ${mcpId} tools saved: ${tools.length} tools`);
|
|
} catch (error) {
|
|
console.error(`Error saving tools for MCP ${mcpId}:`, error);
|
|
}
|
|
}
|
|
}
|