feat: add MCP marketplace and custom scrollbar styles
Add comprehensive CSS styles for the MCP Marketplace interface, including card hover effects, responsive grid layout, modal backdrop blur, category badges, rating stars, install buttons, and loading animations. Also include custom scrollbar styling for the model selector to enhance user experience and visual consistency.
This commit is contained in:
1 parent
d5dd3a9f09
commit
ce010d39ee
31 files changed
+4127
-45
No files matched your search
@@ -0,0 +1,34 @@
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
let db: Database.Database | null = null;
|
||||
|
||||
export function getDatabase(): Database.Database {
|
||||
if (!db) {
|
||||
const dbPath = path.join(process.cwd(), "database", "ollahub.db");
|
||||
|
||||
// Garantir que o diretório existe
|
||||
const dbDir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dbDir)) {
|
||||
fs.mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
|
||||
db = new Database(dbPath);
|
||||
|
||||
// Configurações de performance
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("synchronous = NORMAL");
|
||||
db.pragma("foreign_keys = ON");
|
||||
db.pragma("cache_size = 10000");
|
||||
db.pragma("temp_store = memory");
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
export function closeDatabase(): void {
|
||||
if (db) {
|
||||
db.close();
|
||||
db = null;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,464 @@
|
||||
import { getDatabase } from "@/database/database";
|
||||
|
||||
const db = getDatabase();
|
||||
|
||||
export interface MCPSearchParams {
|
||||
query?: string;
|
||||
category?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
sortBy?: string;
|
||||
order?: "asc" | "desc";
|
||||
}
|
||||
|
||||
export interface MCPCacheItem {
|
||||
id: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
content_name: string;
|
||||
publisher_id: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
subfield?: string;
|
||||
field?: string;
|
||||
rating: number;
|
||||
review_cnt: number;
|
||||
content_tag_list?: string;
|
||||
thumbnail_picture?: string;
|
||||
website?: string;
|
||||
detail_url?: string;
|
||||
ext_info?: any;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export class MCPCacheRepository {
|
||||
private static readonly CACHE_TTL = 24 * 60 * 60 * 1000; // 24 horas em millisegundos
|
||||
|
||||
/**
|
||||
* Verificar se cache está válido (< 24h)
|
||||
*/
|
||||
static isCacheValid(): boolean {
|
||||
try {
|
||||
const lastSync = this.getLastSync();
|
||||
if (!lastSync) return false;
|
||||
|
||||
const now = Date.now();
|
||||
return now - lastSync < this.CACHE_TTL;
|
||||
} catch (error) {
|
||||
console.error("Error checking cache validity:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter timestamp da última sincronização
|
||||
*/
|
||||
static getLastSync(): number | null {
|
||||
try {
|
||||
const stmt = db.prepare(
|
||||
"SELECT value FROM mcp_cache_metadata WHERE key = 'last_sync'"
|
||||
);
|
||||
const result = stmt.get() as { value: string } | undefined;
|
||||
return result ? parseInt(result.value) : null;
|
||||
} catch (error) {
|
||||
console.error("Error getting last sync:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Salvar múltiplos MCPs em batch (performance)
|
||||
*/
|
||||
static saveBatch(mcps: any[]): void {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO mcp_marketplace_cache (
|
||||
id, owner, repo, content_name, publisher_id, description,
|
||||
category, subfield, field, rating, review_cnt, content_tag_list,
|
||||
thumbnail_picture, website, detail_url, ext_info, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
owner = EXCLUDED.owner,
|
||||
repo = EXCLUDED.repo,
|
||||
content_name = EXCLUDED.content_name,
|
||||
publisher_id = EXCLUDED.publisher_id,
|
||||
description = EXCLUDED.description,
|
||||
category = EXCLUDED.category,
|
||||
subfield = EXCLUDED.subfield,
|
||||
field = EXCLUDED.field,
|
||||
rating = EXCLUDED.rating,
|
||||
review_cnt = EXCLUDED.review_cnt,
|
||||
content_tag_list = EXCLUDED.content_tag_list,
|
||||
thumbnail_picture = EXCLUDED.thumbnail_picture,
|
||||
website = EXCLUDED.website,
|
||||
detail_url = EXCLUDED.detail_url,
|
||||
ext_info = EXCLUDED.ext_info,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (const mcp of mcps) {
|
||||
const [owner, repo] = (mcp.id || "").split("/");
|
||||
stmt.run(
|
||||
mcp.id || "",
|
||||
owner || "",
|
||||
repo || "",
|
||||
mcp.content_name || "",
|
||||
mcp.publisher_id || "",
|
||||
mcp.description || null,
|
||||
mcp.category || null,
|
||||
mcp.subfield || null,
|
||||
mcp.field || null,
|
||||
parseFloat(mcp.rating || "0"),
|
||||
parseInt(mcp.review_cnt || "0"),
|
||||
mcp.content_tag_list || null,
|
||||
mcp.thumbnail_picture || null,
|
||||
mcp.website || null,
|
||||
mcp.detail_url || null,
|
||||
mcp.ext_info ? JSON.stringify(mcp.ext_info) : null,
|
||||
now,
|
||||
now
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Saved batch of ${mcps.length} MCPs to cache`);
|
||||
} catch (error) {
|
||||
console.error("Error saving MCP batch:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpar cache antigo
|
||||
*/
|
||||
static clearCache(): void {
|
||||
try {
|
||||
db.exec("DELETE FROM mcp_marketplace_cache");
|
||||
db.exec("DELETE FROM mcp_cache_metadata");
|
||||
console.log("MCP cache cleared");
|
||||
} catch (error) {
|
||||
console.error("Error clearing cache:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar MCPs no cache com filtros
|
||||
*/
|
||||
static search(params: MCPSearchParams): MCPCacheItem[] {
|
||||
try {
|
||||
let query = "SELECT * FROM mcp_marketplace_cache WHERE 1=1";
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const results = stmt.all(...queryParams) as MCPCacheItem[];
|
||||
|
||||
console.log(`Found ${results.length} MCPs in cache for search:`, params);
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error("Error searching MCP cache:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contar total de MCPs no cache
|
||||
*/
|
||||
static count(filters?: { category?: string }): number {
|
||||
try {
|
||||
let query =
|
||||
"SELECT COUNT(*) as count FROM mcp_marketplace_cache WHERE 1=1";
|
||||
const queryParams: any[] = [];
|
||||
|
||||
if (filters?.category) {
|
||||
query += " AND (category = ? OR subfield = ?)";
|
||||
queryParams.push(filters.category, filters.category);
|
||||
}
|
||||
|
||||
const stmt = db.prepare(query);
|
||||
const result = stmt.get(...queryParams) as { count: number };
|
||||
return result.count;
|
||||
} catch (error) {
|
||||
console.error("Error counting MCP cache:", error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualizar metadados do cache
|
||||
*/
|
||||
static updateMetadata(key: string, value: string): void {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO mcp_cache_metadata (key, value, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = EXCLUDED.value,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`);
|
||||
stmt.run(key, value, Date.now());
|
||||
} catch (error) {
|
||||
console.error("Error updating cache metadata:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar se precisa sincronizar
|
||||
*/
|
||||
static needsSync(): boolean {
|
||||
try {
|
||||
// Se não há dados no cache, precisa sincronizar
|
||||
const count = this.count();
|
||||
if (count === 0) return true;
|
||||
|
||||
// Se cache expirou, precisa sincronizar
|
||||
return !this.isCacheValid();
|
||||
} catch (error) {
|
||||
console.error("Error checking if sync is needed:", error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter estatísticas do cache
|
||||
*/
|
||||
static getStats(): {
|
||||
totalItems: number;
|
||||
lastSync: number | null;
|
||||
isValid: boolean;
|
||||
categories: Record<string, number>;
|
||||
} {
|
||||
try {
|
||||
const totalItems = this.count();
|
||||
const lastSync = this.getLastSync();
|
||||
const isValid = this.isCacheValid();
|
||||
|
||||
// Contar por categoria
|
||||
const categoryStmt = db.prepare(`
|
||||
SELECT category, COUNT(*) as count
|
||||
FROM mcp_marketplace_cache
|
||||
WHERE category IS NOT NULL
|
||||
GROUP BY category
|
||||
`);
|
||||
const categoryResults = categoryStmt.all() as Array<{
|
||||
category: string;
|
||||
count: number;
|
||||
}>;
|
||||
|
||||
const categories = categoryResults.reduce((acc, row) => {
|
||||
acc[row.category] = row.count;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return {
|
||||
totalItems,
|
||||
lastSync,
|
||||
isValid,
|
||||
categories,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting cache stats:", error);
|
||||
return {
|
||||
totalItems: 0,
|
||||
lastSync: null,
|
||||
isValid: false,
|
||||
categories: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar se uma tabela existe
|
||||
*/
|
||||
static tableExists(tableName: string): boolean {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name=?
|
||||
`);
|
||||
const result = stmt.get(tableName);
|
||||
return !!result;
|
||||
} catch (error) {
|
||||
console.error("Error checking if table exists:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrair categorias dinamicamente do cache
|
||||
* Retorna categorias principais e outras categorias menos comuns
|
||||
*/
|
||||
static getCategories(): {
|
||||
primary: Array<{ category: string; count: number }>;
|
||||
others: Array<{ category: string; count: number }>;
|
||||
} {
|
||||
try {
|
||||
// Categorias principais definidas (baseadas nos dados reais da API)
|
||||
const primaryCategories = [
|
||||
"MAP",
|
||||
"BROWSER",
|
||||
"OFFICE",
|
||||
"CODE",
|
||||
"DATABASE",
|
||||
"SEARCH",
|
||||
"PAYMENT",
|
||||
"CHART",
|
||||
"FINANCE",
|
||||
"OTHER",
|
||||
];
|
||||
|
||||
// Query para obter todas as categorias com contadores
|
||||
const stmt = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(subfield, category, 'other') as category,
|
||||
COUNT(*) as count
|
||||
FROM mcp_marketplace_cache
|
||||
WHERE COALESCE(subfield, category, 'other') IS NOT NULL
|
||||
GROUP BY COALESCE(subfield, category, 'other')
|
||||
ORDER BY count DESC
|
||||
`);
|
||||
|
||||
const allCategories = stmt.all() as Array<{
|
||||
category: string;
|
||||
count: number;
|
||||
}>;
|
||||
|
||||
// Separar em categorias principais e outras
|
||||
const primary: Array<{ category: string; count: number }> = [];
|
||||
const others: Array<{ category: string; count: number }> = [];
|
||||
|
||||
for (const cat of allCategories) {
|
||||
const normalizedCategory = cat.category.toUpperCase();
|
||||
|
||||
if (primaryCategories.includes(normalizedCategory)) {
|
||||
primary.push({
|
||||
category: normalizedCategory,
|
||||
count: cat.count,
|
||||
});
|
||||
} else {
|
||||
others.push({
|
||||
category: cat.category,
|
||||
count: cat.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ordenar categorias principais por contagem (desc)
|
||||
primary.sort((a, b) => b.count - a.count);
|
||||
|
||||
// Ordenar outras categorias por contagem (desc)
|
||||
others.sort((a, b) => b.count - a.count);
|
||||
|
||||
console.log(
|
||||
`📊 Extracted categories: ${primary.length} primary, ${others.length} others`
|
||||
);
|
||||
|
||||
return { primary, others };
|
||||
} catch (error) {
|
||||
console.error("Error extracting categories:", error);
|
||||
return { primary: [], others: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter estatísticas detalhadas de categorias
|
||||
*/
|
||||
static getCategoryStats(): {
|
||||
totalCategories: number;
|
||||
primaryCategories: number;
|
||||
otherCategories: number;
|
||||
totalMCPs: number;
|
||||
categoryBreakdown: Record<string, number>;
|
||||
} {
|
||||
try {
|
||||
const categories = this.getCategories();
|
||||
const totalMCPs = this.count();
|
||||
|
||||
const categoryBreakdown: Record<string, number> = {};
|
||||
|
||||
// Adicionar categorias principais
|
||||
for (const cat of categories.primary) {
|
||||
categoryBreakdown[cat.category] = cat.count;
|
||||
}
|
||||
|
||||
// Adicionar outras categorias
|
||||
for (const cat of categories.others) {
|
||||
categoryBreakdown[cat.category] = cat.count;
|
||||
}
|
||||
|
||||
return {
|
||||
totalCategories: categories.primary.length + categories.others.length,
|
||||
primaryCategories: categories.primary.length,
|
||||
otherCategories: categories.others.length,
|
||||
totalMCPs,
|
||||
categoryBreakdown,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error getting category stats:", error);
|
||||
return {
|
||||
totalCategories: 0,
|
||||
primaryCategories: 0,
|
||||
otherCategories: 0,
|
||||
totalMCPs: 0,
|
||||
categoryBreakdown: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
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 {
|
||||
const [owner, repo] = mcpId.split('/');
|
||||
if (!owner || !repo) {
|
||||
throw new Error(`Invalid MCP ID format: ${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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,55 @@ CREATE TABLE embeddings (
|
||||
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Tabela para gerenciar MCPs instalados
|
||||
CREATE TABLE IF NOT EXISTS mcp_installations (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner TEXT NOT NULL,
|
||||
repo TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
config JSON NOT NULL,
|
||||
tools JSON,
|
||||
installed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Cache de todos os servidores MCP do marketplace
|
||||
CREATE TABLE IF NOT EXISTS mcp_marketplace_cache (
|
||||
id TEXT PRIMARY KEY,
|
||||
owner TEXT NOT NULL,
|
||||
repo TEXT NOT NULL,
|
||||
content_name TEXT NOT NULL,
|
||||
publisher_id TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT,
|
||||
subfield TEXT,
|
||||
field TEXT,
|
||||
rating REAL DEFAULT 0,
|
||||
review_cnt INTEGER DEFAULT 0,
|
||||
content_tag_list TEXT,
|
||||
thumbnail_picture TEXT,
|
||||
website TEXT,
|
||||
detail_url TEXT,
|
||||
ext_info JSON,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Metadados do cache (última sincronização, total de registros)
|
||||
CREATE TABLE IF NOT EXISTS mcp_cache_metadata (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Índices para performance
|
||||
CREATE INDEX idx_messages_conversation ON messages(conversation_id, timestamp);
|
||||
CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC);
|
||||
CREATE INDEX idx_embeddings_message ON embeddings(message_id);
|
||||
CREATE INDEX idx_mcp_owner_repo ON mcp_installations(owner, repo);
|
||||
CREATE INDEX idx_mcp_installed_at ON mcp_installations(installed_at DESC);
|
||||
CREATE INDEX idx_mcp_cache_category ON mcp_marketplace_cache(category);
|
||||
CREATE INDEX idx_mcp_cache_subfield ON mcp_marketplace_cache(subfield);
|
||||
CREATE INDEX idx_mcp_cache_rating ON mcp_marketplace_cache(rating DESC);
|
||||
CREATE INDEX idx_mcp_cache_owner_repo ON mcp_marketplace_cache(owner, repo);
|
||||
CREATE INDEX idx_mcp_cache_search ON mcp_marketplace_cache(content_name, description);
|
||||
Reference in new issue
Block a user