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
@@ -167,61 +167,72 @@ export class MCPCacheRepository {
|
||||
*/
|
||||
static search(params: MCPSearchParams): MCPCacheItem[] {
|
||||
try {
|
||||
let query = "SELECT * FROM mcp_marketplace_cache WHERE 1=1";
|
||||
let query: string;
|
||||
const queryParams: any[] = [];
|
||||
|
||||
// Filtro por categoria
|
||||
if (params.category) {
|
||||
query += " AND (category = ? OR subfield = ?)";
|
||||
queryParams.push(params.category, params.category);
|
||||
}
|
||||
|
||||
// Filtro por busca textual
|
||||
if (params.query) {
|
||||
query +=
|
||||
" AND (content_name LIKE ? OR description LIKE ? OR content_tag_list LIKE ?)";
|
||||
const searchTerm = `%${params.query}%`;
|
||||
queryParams.push(searchTerm, searchTerm, searchTerm);
|
||||
// Usar FTS5 para busca textual
|
||||
query = `
|
||||
SELECT c.* FROM mcp_marketplace_cache c
|
||||
INNER JOIN mcp_search_fts f ON c.rowid = f.rowid
|
||||
WHERE mcp_search_fts MATCH ?
|
||||
`;
|
||||
|
||||
// Escapar caracteres especiais e adicionar wildcard
|
||||
const searchTerm = params.query.replace(/[^a-zA-Z0-9\s]/g, " ").trim();
|
||||
queryParams.push(`${searchTerm}*`); // Prefix search
|
||||
|
||||
// Filtro de categoria (se houver)
|
||||
if (params.category) {
|
||||
query += " AND (c.category = ? OR c.subfield = ?)";
|
||||
queryParams.push(params.category, params.category);
|
||||
}
|
||||
|
||||
// Ordenar por relevância FTS + rating
|
||||
query += " ORDER BY f.rank, c.rating DESC";
|
||||
} else {
|
||||
// Query sem busca textual (usar índices normais)
|
||||
query = "SELECT * FROM mcp_marketplace_cache WHERE 1=1";
|
||||
|
||||
if (params.category) {
|
||||
query += " AND (category = ? OR subfield = ?)";
|
||||
queryParams.push(params.category, params.category);
|
||||
}
|
||||
|
||||
// Ordenação por índice apropriado
|
||||
const order = params.order || "desc";
|
||||
let orderClause = "";
|
||||
|
||||
switch (params.sortBy) {
|
||||
case "rating":
|
||||
orderClause = `ORDER BY rating ${order.toUpperCase()}`;
|
||||
break;
|
||||
case "downloads":
|
||||
orderClause = `ORDER BY review_cnt ${order.toUpperCase()}`;
|
||||
break;
|
||||
case "name":
|
||||
orderClause = `ORDER BY content_name ${order.toUpperCase()}`;
|
||||
break;
|
||||
case "recent":
|
||||
orderClause = `ORDER BY updated_at ${order.toUpperCase()}`;
|
||||
break;
|
||||
case "total_ratings":
|
||||
orderClause = `ORDER BY review_cnt ${order.toUpperCase()}`;
|
||||
break;
|
||||
case "updated_at":
|
||||
orderClause = `ORDER BY updated_at ${order.toUpperCase()}`;
|
||||
break;
|
||||
default:
|
||||
orderClause = `ORDER BY rating DESC`; // Padrão
|
||||
}
|
||||
|
||||
query += ` ${orderClause}`;
|
||||
}
|
||||
|
||||
// Ordenação dinâmica
|
||||
const order = params.order || "desc"; // Padrão descrescente
|
||||
let orderClause = "";
|
||||
|
||||
switch (params.sortBy) {
|
||||
case "rating":
|
||||
orderClause = `ORDER BY rating ${order.toUpperCase()}`;
|
||||
break;
|
||||
case "downloads":
|
||||
orderClause = `ORDER BY review_cnt ${order.toUpperCase()}`;
|
||||
break;
|
||||
case "name":
|
||||
orderClause = `ORDER BY content_name ${order.toUpperCase()}`;
|
||||
break;
|
||||
case "recent":
|
||||
orderClause = `ORDER BY updated_at ${order.toUpperCase()}`;
|
||||
break;
|
||||
case "total_ratings":
|
||||
orderClause = `ORDER BY review_cnt ${order.toUpperCase()}`;
|
||||
break;
|
||||
case "updated_at":
|
||||
orderClause = `ORDER BY updated_at ${order.toUpperCase()}`;
|
||||
break;
|
||||
default:
|
||||
orderClause = `ORDER BY rating DESC`; // Padrão
|
||||
}
|
||||
|
||||
query += ` ${orderClause}`;
|
||||
|
||||
// Paginação
|
||||
if (params.limit) {
|
||||
query += " LIMIT ?";
|
||||
queryParams.push(params.limit);
|
||||
|
||||
if (params.offset) {
|
||||
query += " OFFSET ?";
|
||||
queryParams.push(params.offset);
|
||||
}
|
||||
query += " LIMIT ? OFFSET ?";
|
||||
queryParams.push(params.limit, params.offset || 0);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
@@ -479,4 +490,55 @@ export class MCPCacheRepository {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar MCP por owner e repo
|
||||
*/
|
||||
static findByOwnerRepo(owner: string, repo: string): MCPCacheItem | null {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM mcp_marketplace_cache
|
||||
WHERE owner = ? AND repo = ?
|
||||
`);
|
||||
const result = stmt.get(owner, repo) as MCPCacheItem | undefined;
|
||||
return result || null;
|
||||
} catch (error) {
|
||||
console.error("Error finding MCP by owner/repo:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar MCP por nome
|
||||
*/
|
||||
static findByName(name: string): MCPCacheItem | null {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM mcp_marketplace_cache
|
||||
WHERE content_name = ? OR repo = ?
|
||||
`);
|
||||
const result = stmt.get(name, name) as MCPCacheItem | undefined;
|
||||
return result || null;
|
||||
} catch (error) {
|
||||
console.error("Error finding MCP by name:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar MCP por ID
|
||||
*/
|
||||
static findById(id: string): MCPCacheItem | null {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM mcp_marketplace_cache
|
||||
WHERE id = ?
|
||||
`);
|
||||
const result = stmt.get(id) as MCPCacheItem | undefined;
|
||||
return result || null;
|
||||
} catch (error) {
|
||||
console.error("Error finding MCP by ID:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+214
-20
@@ -24,9 +24,18 @@ export class MCPRepository {
|
||||
*/
|
||||
static saveInstallation(mcpId: string, config: any, tools: any[]): void {
|
||||
try {
|
||||
const [owner, repo] = mcpId.split('/');
|
||||
if (!owner || !repo) {
|
||||
throw new Error(`Invalid MCP ID format: ${mcpId}`);
|
||||
// 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(`
|
||||
@@ -58,7 +67,7 @@ export class MCPRepository {
|
||||
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 {
|
||||
@@ -75,9 +84,11 @@ export class MCPRepository {
|
||||
*/
|
||||
static listInstalled(): string[] {
|
||||
try {
|
||||
const stmt = db.prepare("SELECT id FROM mcp_installations ORDER BY installed_at DESC");
|
||||
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);
|
||||
return results.map((row) => row.id);
|
||||
} catch (error) {
|
||||
console.error("Error listing installed MCPs:", error);
|
||||
return [];
|
||||
@@ -89,9 +100,11 @@ export class MCPRepository {
|
||||
*/
|
||||
static getLocalConfig(mcpId: string): any {
|
||||
try {
|
||||
const stmt = db.prepare("SELECT config FROM mcp_installations WHERE id = ?");
|
||||
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);
|
||||
}
|
||||
@@ -107,9 +120,11 @@ export class MCPRepository {
|
||||
*/
|
||||
static getLocalTools(mcpId: string): any[] {
|
||||
try {
|
||||
const stmt = db.prepare("SELECT tools FROM mcp_installations WHERE id = ?");
|
||||
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);
|
||||
}
|
||||
@@ -130,12 +145,12 @@ export class MCPRepository {
|
||||
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)
|
||||
tools: JSON.parse(result.tools),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -155,11 +170,11 @@ export class MCPRepository {
|
||||
FROM mcp_installations ORDER BY installed_at DESC
|
||||
`);
|
||||
const results = stmt.all() as any[];
|
||||
|
||||
return results.map(row => ({
|
||||
|
||||
return results.map((row) => ({
|
||||
...row,
|
||||
config: JSON.parse(row.config),
|
||||
tools: JSON.parse(row.tools)
|
||||
tools: JSON.parse(row.tools),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("Error listing installed MCPs with details:", error);
|
||||
@@ -172,19 +187,48 @@ export class MCPRepository {
|
||||
*/
|
||||
static updateTimestamp(mcpId: string): void {
|
||||
try {
|
||||
const stmt = db.prepare("UPDATE mcp_installations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?");
|
||||
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 };
|
||||
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);
|
||||
@@ -197,7 +241,9 @@ export class MCPRepository {
|
||||
*/
|
||||
static getInstallationStats(): any {
|
||||
try {
|
||||
const totalStmt = db.prepare("SELECT COUNT(*) as total FROM mcp_installations");
|
||||
const totalStmt = db.prepare(
|
||||
"SELECT COUNT(*) as total FROM mcp_installations"
|
||||
);
|
||||
const totalResult = totalStmt.get() as { total: number };
|
||||
|
||||
const recentStmt = db.prepare(`
|
||||
@@ -211,16 +257,164 @@ export class MCPRepository {
|
||||
SELECT COUNT(DISTINCT owner) as unique_owners
|
||||
FROM mcp_installations
|
||||
`);
|
||||
const categoriesResult = categoriesStmt.get() as { unique_owners: number };
|
||||
const categoriesResult = categoriesStmt.get() as {
|
||||
unique_owners: number;
|
||||
};
|
||||
|
||||
return {
|
||||
total: totalResult.total,
|
||||
recent: recentResult.recent,
|
||||
uniqueOwners: categoriesResult.unique_owners
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user