refactor(api): remove MCP marketplace cache integration
Removed dependencies on MCPCacheRepository and PulseMCPService from MCP routes to simplify installation and listing logic, avoiding reliance on potentially stale cached data and streamlining the codebase. Updated available-for-chat filter to include MCPs without explicit status.
This commit is contained in:
1 parent
4739a13458
commit
f96d4ed696
34 files changed
+260
-3775
No files matched your search
@@ -15,7 +15,7 @@ export async function GET() {
|
||||
const allMcps = MCPRepository.getAllInstalled();
|
||||
|
||||
const availableMcps = allMcps
|
||||
.filter((mcp: any) => mcp.status === "ready")
|
||||
.filter((mcp: any) => !mcp.status || mcp.status === "ready")
|
||||
.map((mcp: any) => {
|
||||
const tools = MCPExecutor.getAvailableTools(mcp.id);
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* GET /api/mcp/categories - Obter categorias dinâmicas do cache
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
console.log("📊 Fetching dynamic categories from cache...");
|
||||
|
||||
// Verificar se cache existe e tem dados
|
||||
const cacheExists = MCPCacheRepository.tableExists("mcp_marketplace_cache");
|
||||
if (!cacheExists) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "Cache not initialized",
|
||||
categories: {
|
||||
primary: [],
|
||||
others: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Obter categorias dinâmicas
|
||||
const categories = MCPCacheRepository.getCategories();
|
||||
const stats = MCPCacheRepository.getCategoryStats();
|
||||
|
||||
// Verificar se há dados no cache
|
||||
if (stats.totalMCPs === 0) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "No MCPs found in cache",
|
||||
categories: {
|
||||
primary: [],
|
||||
others: [],
|
||||
},
|
||||
stats,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`📊 Categories extracted: ${categories.primary.length} primary, ${categories.others.length} others`
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
categories,
|
||||
stats,
|
||||
lastSync: MCPCacheRepository.getLastSync(),
|
||||
cacheValid: MCPCacheRepository.isCacheValid(),
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching categories:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message || "Failed to fetch categories",
|
||||
categories: {
|
||||
primary: [],
|
||||
others: [],
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ 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,
|
||||
@@ -71,17 +69,6 @@ export async function POST(request: Request) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
|
||||
// Marketplace cache removido
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -11,48 +11,20 @@ export async function GET() {
|
||||
// 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 || "",
|
||||
};
|
||||
});
|
||||
// Retornar dados de instalados sem enriquecimento de marketplace
|
||||
const enrichedMcps = installedMcps.map((mcp) => ({
|
||||
...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`);
|
||||
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
MCPProvider,
|
||||
MCPSearchParams,
|
||||
PulseMCPSearchParams,
|
||||
} from "@/lib/types/mcp";
|
||||
import { PulseMCPService } from "@/lib/services/pulsemcp";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
|
||||
import { MCPSyncService } from "@/lib/services/mcp-sync";
|
||||
|
||||
const PULSEMCP_API_BASE = "https://api.pulsemcp.com/v0beta";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const category = searchParams.get("category") || undefined;
|
||||
const search = searchParams.get("search") || undefined;
|
||||
const sort =
|
||||
(searchParams.get("sort") as MCPSearchParams["sort"]) || "rating";
|
||||
const order = (searchParams.get("order") as "asc" | "desc") || "desc";
|
||||
const page = parseInt(searchParams.get("page") || "0");
|
||||
const limit = parseInt(searchParams.get("limit") || "2000"); // Aumentado para 2000
|
||||
|
||||
console.log("MCP List API called with params:", {
|
||||
category,
|
||||
search,
|
||||
sort,
|
||||
order,
|
||||
page,
|
||||
limit,
|
||||
});
|
||||
|
||||
// 1. Verificar se cache do DB é válido
|
||||
const cacheValid = MCPCacheRepository.isCacheValid();
|
||||
|
||||
if (!cacheValid) {
|
||||
// Iniciar sincronização em background se necessário
|
||||
if (!MCPSyncService.isSyncing()) {
|
||||
console.log("Cache invalid, starting background sync...");
|
||||
MCPSyncService.backgroundSync().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Tentar buscar do banco de dados primeiro
|
||||
const cachedMcps = MCPCacheRepository.search({
|
||||
query: search,
|
||||
category: category !== "all" ? category : undefined,
|
||||
limit,
|
||||
offset: page * limit,
|
||||
sortBy: sort,
|
||||
order: order,
|
||||
});
|
||||
|
||||
if (cachedMcps.length > 0 || cacheValid) {
|
||||
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) => {
|
||||
// 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();
|
||||
const mcpsWithStatus = mcps.map((mcp) => ({
|
||||
...mcp,
|
||||
installed: installedMCPs.includes(mcp.id),
|
||||
}));
|
||||
|
||||
const total = MCPCacheRepository.count({
|
||||
category: category !== "all" ? category : undefined,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcps: mcpsWithStatus,
|
||||
total,
|
||||
hasMore: (page + 1) * limit < total,
|
||||
cached: true,
|
||||
lastSync: MCPCacheRepository.getLastSync(),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Fallback: buscar da API PulseMCP se cache vazio
|
||||
console.log("Cache empty, falling back to PulseMCP API");
|
||||
|
||||
const pulseMCPParams: PulseMCPSearchParams = {
|
||||
query: search,
|
||||
count_per_page: limit,
|
||||
offset: page * limit,
|
||||
};
|
||||
|
||||
console.log("Calling PulseMCP API with params:", pulseMCPParams);
|
||||
|
||||
const response = await fetch(
|
||||
`${PULSEMCP_API_BASE}/servers?${new URLSearchParams({
|
||||
...(search && { query: search }),
|
||||
count_per_page: limit.toString(),
|
||||
offset: (page * limit).toString(),
|
||||
})}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0 (https://ollahub.com)",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`PulseMCP API error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const mcps = data.servers.map((item: any) =>
|
||||
PulseMCPService.transformToMCPProvider(item)
|
||||
);
|
||||
|
||||
// Adicionar status de instalação
|
||||
const installedMCPs = MCPRepository.listInstalled();
|
||||
const mcpsWithStatus = mcps.map((mcp: MCPProvider) => ({
|
||||
...mcp,
|
||||
installed: installedMCPs.includes(mcp.id),
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcps: mcpsWithStatus,
|
||||
total: data.total_count,
|
||||
hasMore: data.total_count > (page + 1) * limit,
|
||||
cached: false,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching MCPs from PulseMCP:", error);
|
||||
|
||||
// Fallback para dados mock em caso de erro
|
||||
console.log("Falling back to mock data due to API error");
|
||||
|
||||
const mockMCPs: MCPProvider[] = [
|
||||
{
|
||||
id: "google-maps/google-maps",
|
||||
name: "Google Maps",
|
||||
author: "Google",
|
||||
description:
|
||||
"Google Maps Location services, directions, and place details",
|
||||
version: "1.0.0",
|
||||
category: "map",
|
||||
tags: ["maps", "location", "directions"],
|
||||
rating: 4.5,
|
||||
totalRatings: 2,
|
||||
repository:
|
||||
"https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps",
|
||||
installed: false,
|
||||
subfield: "MAP",
|
||||
field: "MCP SERVER",
|
||||
config: [],
|
||||
tools: [],
|
||||
},
|
||||
];
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcps: mockMCPs,
|
||||
total: mockMCPs.length,
|
||||
hasMore: false,
|
||||
fallback: true,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
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 {
|
||||
@@ -16,67 +15,9 @@ export async function GET(request: Request) {
|
||||
|
||||
console.log(`Fetching server config for: ${mcpId}`);
|
||||
|
||||
// Primeiro, tentar buscar do cache local (PulseMCP)
|
||||
const [owner, repo] = mcpId.includes("/") ? mcpId.split("/") : ["", mcpId];
|
||||
// Marketplace removido: pular cache local
|
||||
|
||||
// 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
|
||||
// Tentar DeepNLP
|
||||
try {
|
||||
const serverConfig = await DeepNLPService.getServerConfig(mcpId);
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPSyncService } from "@/lib/services/mcp-sync";
|
||||
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* POST /api/mcp/sync - Iniciar sincronização manual
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
if (MCPSyncService.isSyncing()) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "Sync already in progress"
|
||||
}, { status: 409 });
|
||||
}
|
||||
|
||||
console.log("🔄 Starting manual MCP sync...");
|
||||
|
||||
const result = await MCPSyncService.syncAll((progress) => {
|
||||
console.log(`📥 Progress: ${progress.current}/${progress.total} (${progress.page}/${progress.totalPages} pages)`);
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: result.success,
|
||||
totalDownloaded: result.totalDownloaded,
|
||||
errors: result.errors,
|
||||
duration: result.duration
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Manual sync failed:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.message || "Failed to sync MCPs" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/mcp/sync - Obter status da sincronização
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const stats = MCPCacheRepository.getStats();
|
||||
const healthCheck = await MCPSyncService.healthCheck();
|
||||
|
||||
return NextResponse.json({
|
||||
isSyncing: MCPSyncService.isSyncing(),
|
||||
lastSync: stats.lastSync,
|
||||
totalCached: stats.totalItems,
|
||||
needsSync: MCPCacheRepository.needsSync(),
|
||||
cacheValid: stats.isValid,
|
||||
categories: stats.categories,
|
||||
healthCheck
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error getting sync status:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.message || "Failed to get sync status" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/mcp/sync - Cancelar sincronização atual
|
||||
*/
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
if (!MCPSyncService.isSyncing()) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: "No sync in progress"
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
MCPSyncService.cancelSync();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Sync cancelled successfully"
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error cancelling sync:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.message || "Failed to cancel sync" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { PulseMCPService } from "@/lib/services/pulsemcp";
|
||||
|
||||
const PULSEMCP_API_BASE = "https://api.pulsemcp.com/v0beta";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const limit = parseInt(searchParams.get("limit") || "20");
|
||||
const offset = parseInt(searchParams.get("offset") || "0");
|
||||
|
||||
console.log("🔍 Testando PulseMCP API diretamente...");
|
||||
|
||||
// Chamar API PulseMCP diretamente
|
||||
const response = await fetch(
|
||||
`${PULSEMCP_API_BASE}/servers?${new URLSearchParams({
|
||||
count_per_page: limit.toString(),
|
||||
offset: offset.toString(),
|
||||
})}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0 (https://ollahub.com)",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`PulseMCP API error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(
|
||||
`✅ PulseMCP API respondeu com ${data.servers.length} servidores`
|
||||
);
|
||||
|
||||
// Transformar para formato interno
|
||||
const mcps = data.servers.map((item: any) =>
|
||||
PulseMCPService.transformToMCPProvider(item)
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcps: mcps,
|
||||
total: data.total_count,
|
||||
hasMore: data.total_count > offset + limit,
|
||||
source: "pulsemcp-direct",
|
||||
message: `Listando ${mcps.length} servidores diretamente da API PulseMCP`,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("❌ Erro ao chamar PulseMCP API:", error);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message,
|
||||
mcps: [],
|
||||
total: 0,
|
||||
source: "pulsemcp-direct-error",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,9 @@ export async function POST(req: NextRequest) {
|
||||
async start(controller) {
|
||||
let isClosed = false;
|
||||
let accumulatedResponse = "";
|
||||
let toolExecuted = false;
|
||||
let lastExtractedToolCall: { name: string; parameters: any } | null =
|
||||
null;
|
||||
|
||||
const safeEnqueue = (data: string) => {
|
||||
if (!isClosed) {
|
||||
@@ -95,14 +98,24 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
try {
|
||||
const ollamaMessages = convertToOllamaMessages(messages);
|
||||
console.log("🗣️ [CHAT] Messages ->", ollamaMessages);
|
||||
|
||||
// Merge system prompt com tools prompt
|
||||
const finalSystemPrompt = toolsPrompt
|
||||
? `${system || ""}\n\n${toolsPrompt}`
|
||||
: system;
|
||||
if (toolsPrompt) console.log("🧩 [CHAT] Tools prompt injected");
|
||||
|
||||
// Preparar parâmetros para chat
|
||||
const chatParams = capabilities.supportsNativeTools ? { tools } : {};
|
||||
if (capabilities.supportsNativeTools) {
|
||||
console.log(
|
||||
"🧰 [CHAT] Native tools enabled:",
|
||||
tools?.map((t: any) => t.function?.name)
|
||||
);
|
||||
} else {
|
||||
console.log("🧰 [CHAT] Prompt-engineering tools mode");
|
||||
}
|
||||
|
||||
const response = await chatWithStream(
|
||||
model,
|
||||
@@ -111,28 +124,72 @@ export async function POST(req: NextRequest) {
|
||||
finalSystemPrompt,
|
||||
capabilities.supportsNativeTools ? tools : undefined
|
||||
);
|
||||
console.log("🌊 [CHAT] Streaming started for model:", model);
|
||||
|
||||
const normalizeParameters = (params: any) => {
|
||||
try {
|
||||
if (typeof params === "string") {
|
||||
params = JSON.parse(params);
|
||||
}
|
||||
} catch {
|
||||
// ignora erro de parse e mantém original
|
||||
}
|
||||
if (params && typeof params === "object") {
|
||||
const cleaned: Record<string, any> = {};
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (v && typeof v === "object" && Object.keys(v).length === 0) {
|
||||
// remove objetos vazios que costumam vir de UIs (ex.: count: {})
|
||||
continue;
|
||||
}
|
||||
cleaned[k] = v;
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
return params ?? {};
|
||||
};
|
||||
|
||||
for await (const chunk of response) {
|
||||
console.log("📦 [CHAT stream chunk]", JSON.stringify(chunk));
|
||||
// Verificar se há tool calls (modelos com suporte nativo)
|
||||
if (
|
||||
chunk.message?.tool_calls &&
|
||||
chunk.message.tool_calls.length > 0
|
||||
(chunk.message?.tool_calls &&
|
||||
chunk.message.tool_calls.length > 0) ||
|
||||
(Array.isArray((chunk as any).tool_calls) &&
|
||||
(chunk as any).tool_calls.length > 0)
|
||||
) {
|
||||
console.log("🔧 Model requested tool calls (native)");
|
||||
|
||||
for (const toolCall of chunk.message.tool_calls) {
|
||||
const nativeCalls =
|
||||
chunk.message?.tool_calls || (chunk as any).tool_calls || [];
|
||||
for (const toolCall of nativeCalls) {
|
||||
const toolKey = toolCall.function.name;
|
||||
const toolInfo = mcpToolsMap.get(toolKey);
|
||||
console.log(
|
||||
"🧭 [CHAT] Native tool call:",
|
||||
toolKey,
|
||||
"->",
|
||||
toolInfo
|
||||
);
|
||||
|
||||
if (toolInfo) {
|
||||
// Executar tool
|
||||
const result = await MCPExecutor.executeMCPTool(
|
||||
toolInfo.mcpId,
|
||||
toolInfo.toolName,
|
||||
toolCall.function.arguments || {}
|
||||
normalizeParameters(
|
||||
toolCall.function.arguments ||
|
||||
toolCall.function.parameters ||
|
||||
{}
|
||||
)
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
toolExecuted = true;
|
||||
console.log("✅ [CHAT] Tool executed successfully", {
|
||||
mcpId: toolInfo.mcpId,
|
||||
toolName: toolInfo.toolName,
|
||||
executionTime: result.executionTime,
|
||||
});
|
||||
// Enviar indicador de execução de tool
|
||||
safeEnqueue(
|
||||
JSON.stringify({
|
||||
@@ -149,6 +206,10 @@ export async function POST(req: NextRequest) {
|
||||
toolInfo.toolName,
|
||||
result.result
|
||||
);
|
||||
console.log(
|
||||
"🧩 [CHAT] Injecting tool result into context (length)",
|
||||
String(resultMessage).length
|
||||
);
|
||||
|
||||
// Fazer nova chamada ao modelo com o resultado
|
||||
const updatedMessages = [
|
||||
@@ -189,13 +250,23 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Se não há suporte nativo, verificar se resposta contém tool call via prompt engineering
|
||||
if (!capabilities.supportsNativeTools && accumulatedResponse) {
|
||||
// Verificar se resposta contém tool call via prompt engineering (fallback também para nativo)
|
||||
if (accumulatedResponse) {
|
||||
console.log(
|
||||
"🧾 [CHAT] Accumulated response length:",
|
||||
accumulatedResponse.length
|
||||
);
|
||||
console.log(
|
||||
"🧾 [CHAT] Accumulated preview:",
|
||||
accumulatedResponse.slice(0, 400)
|
||||
);
|
||||
const extractedToolCall =
|
||||
extractToolCallFromResponse(accumulatedResponse);
|
||||
|
||||
if (extractedToolCall) {
|
||||
console.log("🔧 Model requested tool call (prompt engineering)");
|
||||
lastExtractedToolCall = extractedToolCall;
|
||||
console.log("🧭 [CHAT] Extracted tool call:", extractedToolCall);
|
||||
|
||||
// Encontrar MCP e tool
|
||||
let foundToolInfo: { mcpId: string; toolName: string } | null =
|
||||
@@ -216,10 +287,11 @@ export async function POST(req: NextRequest) {
|
||||
const result = await MCPExecutor.executeMCPTool(
|
||||
foundToolInfo.mcpId,
|
||||
foundToolInfo.toolName,
|
||||
extractedToolCall.parameters
|
||||
normalizeParameters(extractedToolCall.parameters)
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
toolExecuted = true;
|
||||
// Limpar resposta anterior (que continha o JSON)
|
||||
safeEnqueue(JSON.stringify({ clearPrevious: true }) + "\n");
|
||||
|
||||
@@ -266,6 +338,10 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
"🕵️ [CHAT] No tool call extracted from accumulated response"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
SettingsButton,
|
||||
SettingsModal,
|
||||
} from "@/components/settings/SettingsModal";
|
||||
import { MCPMarketplaceModal } from "@/components/mcp/MCPMarketplaceModal";
|
||||
import { MCPManualInstallModal } from "@/components/mcp/MCPManualInstallModal";
|
||||
import { Store } from "lucide-react";
|
||||
|
||||
interface ClientHomeProps {
|
||||
@@ -22,7 +22,7 @@ export function ClientHome({ offline, models }: ClientHomeProps) {
|
||||
const [currentConversationId, setCurrentConversationId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [showMCPMarketplace, setShowMCPMarketplace] = useState(false);
|
||||
const [showManualInstall, setShowManualInstall] = useState(false);
|
||||
|
||||
const handleConversationCreated = () => {
|
||||
// Incrementar trigger para forçar atualização da sidebar
|
||||
@@ -49,11 +49,11 @@ export function ClientHome({ offline, models }: ClientHomeProps) {
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowMCPMarketplace(true)}
|
||||
onClick={() => setShowManualInstall(true)}
|
||||
className="flex items-center gap-2 px-3 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-md hover:bg-[var(--background)] transition-colors text-sm"
|
||||
>
|
||||
<Store className="h-4 w-4" />
|
||||
MCP Marketplace
|
||||
Install MCP Manually
|
||||
</button>
|
||||
<SettingsButton onOpen={() => setOpen(true)} />
|
||||
<ModeToggle />
|
||||
@@ -77,9 +77,9 @@ export function ClientHome({ offline, models }: ClientHomeProps) {
|
||||
</div>
|
||||
</main>
|
||||
<SettingsModal isOpen={open} onClose={() => setOpen(false)} />
|
||||
<MCPMarketplaceModal
|
||||
isOpen={showMCPMarketplace}
|
||||
onClose={() => setShowMCPMarketplace(false)}
|
||||
<MCPManualInstallModal
|
||||
isOpen={showManualInstall}
|
||||
onClose={() => setShowManualInstall(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
|
||||
interface MCPCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface MCPCategoryFilterProps {
|
||||
primaryCategories: MCPCategory[];
|
||||
otherCategories: MCPCategory[];
|
||||
selectedCategory: string;
|
||||
onSelectCategory: (categoryId: string) => void;
|
||||
}
|
||||
|
||||
export function MCPCategoryFilter({
|
||||
primaryCategories,
|
||||
otherCategories,
|
||||
selectedCategory,
|
||||
onSelectCategory,
|
||||
}: MCPCategoryFilterProps) {
|
||||
const [showOthers, setShowOthers] = useState(false);
|
||||
|
||||
const getCategoryIcon = (categoryName: string) => {
|
||||
const iconMap: Record<string, string> = {
|
||||
MAP: "🗺️",
|
||||
BROWSER: "🌐",
|
||||
OFFICE: "📄",
|
||||
CODE: "💻",
|
||||
DATABASE: "🗄️",
|
||||
SEARCH: "🔍",
|
||||
PAYMENT: "💳",
|
||||
CHART: "📊",
|
||||
FINANCE: "💰",
|
||||
OTHER: "📦",
|
||||
};
|
||||
return iconMap[categoryName.toUpperCase()] || "📦";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold text-sm text-[var(--foreground)] mb-3">
|
||||
Categories
|
||||
</h3>
|
||||
|
||||
{/* All Categories */}
|
||||
<button
|
||||
onClick={() => onSelectCategory("all")}
|
||||
className={`w-full flex items-center justify-between px-3 py-2 rounded-md text-sm transition-colors ${
|
||||
selectedCategory === "all"
|
||||
? "mcp-category-active"
|
||||
: "hover:bg-[var(--surface)] text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-2">📦 All Categories</span>
|
||||
<span className="text-xs bg-[var(--background)] px-2 py-1 rounded-full">
|
||||
{primaryCategories.reduce((sum, cat) => sum + cat.count, 0) +
|
||||
otherCategories.reduce((sum, cat) => sum + cat.count, 0)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Primary Categories */}
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-xs font-medium text-[var(--muted-foreground)] uppercase tracking-wide">
|
||||
Principais
|
||||
</h4>
|
||||
{primaryCategories.map((category) => (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => onSelectCategory(category.id)}
|
||||
className={`w-full flex items-center justify-between px-3 py-2 rounded-md text-sm transition-colors ${
|
||||
selectedCategory === category.id
|
||||
? "mcp-category-active"
|
||||
: "hover:bg-[var(--surface)] text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{getCategoryIcon(category.name)} {category.name}
|
||||
</span>
|
||||
<span className="text-xs bg-[var(--background)] px-2 py-1 rounded-full">
|
||||
{category.count}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Other Categories */}
|
||||
{otherCategories.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
onClick={() => setShowOthers(!showOthers)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 rounded-md text-sm transition-colors hover:bg-[var(--surface)] text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{showOthers ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
<span className="text-xs font-medium uppercase tracking-wide">
|
||||
Outras ({otherCategories.length})
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{showOthers && (
|
||||
<div className="ml-4 space-y-1">
|
||||
{otherCategories.map((category) => (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => onSelectCategory(category.id)}
|
||||
className={`w-full flex items-center justify-between px-3 py-2 rounded-md text-sm transition-colors ${
|
||||
selectedCategory === category.id
|
||||
? "mcp-category-active"
|
||||
: "hover:bg-[var(--surface)] text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
📦 {category.name}
|
||||
</span>
|
||||
<span className="text-xs bg-[var(--background)] px-2 py-1 rounded-full">
|
||||
{category.count}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -154,9 +154,9 @@ export function MCPManualInstallModal({
|
||||
How to use
|
||||
</h3>
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300 mb-3">
|
||||
Paste your MCP server configuration in Claude Desktop format
|
||||
(claude_desktop_config.json). You can install custom MCPs or MCPs
|
||||
not available in the marketplace.
|
||||
Cole a configuração do servidor MCP no formato do Claude Desktop
|
||||
(claude_desktop_config.json). Apenas instalação manual é suportada
|
||||
neste projeto.
|
||||
</p>
|
||||
<button
|
||||
onClick={handlePasteExample}
|
||||
@@ -273,7 +273,7 @@ export function MCPManualInstallModal({
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
|
||||
<AlertCircle className="h-5 w-5 text-red-500 shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h4 className="font-medium text-red-900 dark:text-red-100">
|
||||
Installation Error
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,74 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Star } from "lucide-react";
|
||||
|
||||
interface MCPRatingStarsProps {
|
||||
rating: number;
|
||||
size?: "sm" | "md" | "lg";
|
||||
showNumber?: boolean;
|
||||
}
|
||||
|
||||
export function MCPRatingStars({
|
||||
rating,
|
||||
size = "sm",
|
||||
showNumber = true,
|
||||
}: MCPRatingStarsProps) {
|
||||
const sizeClasses = {
|
||||
sm: "h-3 w-3",
|
||||
md: "h-4 w-4",
|
||||
lg: "h-5 w-5",
|
||||
};
|
||||
|
||||
const stars = [];
|
||||
const fullStars = Math.floor(rating);
|
||||
const hasHalfStar = rating % 1 >= 0.5;
|
||||
|
||||
// Renderizar estrelas cheias
|
||||
for (let i = 0; i < fullStars; i++) {
|
||||
stars.push(
|
||||
<Star
|
||||
key={i}
|
||||
className={`${sizeClasses[size]} fill-yellow-400 text-yellow-400`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Renderizar meia estrela se necessário
|
||||
if (hasHalfStar) {
|
||||
stars.push(
|
||||
<div key="half" className="relative">
|
||||
<Star className={`${sizeClasses[size]} text-gray-300`} />
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{ width: "50%" }}
|
||||
>
|
||||
<Star
|
||||
className={`${sizeClasses[size]} fill-yellow-400 text-yellow-400`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Renderizar estrelas vazias
|
||||
const emptyStars = 5 - Math.ceil(rating);
|
||||
for (let i = 0; i < emptyStars; i++) {
|
||||
stars.push(
|
||||
<Star
|
||||
key={`empty-${i}`}
|
||||
className={`${sizeClasses[size]} text-gray-300`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex">{stars}</div>
|
||||
{showNumber && (
|
||||
<span className="text-sm text-[var(--muted-foreground)] ml-1">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
interface MCPSearchBarProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSearch: () => void;
|
||||
}
|
||||
|
||||
export function MCPSearchBar({ value, onChange, onSearch }: MCPSearchBarProps) {
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
onSearch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-[var(--muted-foreground)]" />
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Search MCPs..."
|
||||
className="w-full pl-10 pr-4 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent mcp-search-input"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,544 +0,0 @@
|
||||
import { getDatabase } from "@/database/database";
|
||||
import { PulseMCPService } from "@/lib/services/pulsemcp";
|
||||
|
||||
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 pulseMCPItem of mcps) {
|
||||
// Transformar dados PulseMCP para formato interno
|
||||
const mcpProvider =
|
||||
PulseMCPService.transformToMCPProvider(pulseMCPItem);
|
||||
|
||||
// Extrair owner/repo do package_name ou usar name como fallback
|
||||
const packageName = pulseMCPItem.package_name || pulseMCPItem.name;
|
||||
const [owner, repo] = packageName.includes("/")
|
||||
? packageName.split("/")
|
||||
: ["", packageName];
|
||||
|
||||
stmt.run(
|
||||
mcpProvider.id,
|
||||
owner,
|
||||
repo,
|
||||
mcpProvider.name,
|
||||
mcpProvider.author,
|
||||
mcpProvider.description,
|
||||
mcpProvider.category,
|
||||
mcpProvider.subfield,
|
||||
mcpProvider.field,
|
||||
mcpProvider.rating,
|
||||
mcpProvider.totalRatings,
|
||||
mcpProvider.tags.join(","),
|
||||
null, // Não temos ícones disponíveis
|
||||
mcpProvider.homepage || null,
|
||||
pulseMCPItem.source_code_url || null,
|
||||
JSON.stringify({
|
||||
package_registry: pulseMCPItem.package_registry,
|
||||
package_name: pulseMCPItem.package_name,
|
||||
github_stars: pulseMCPItem.github_stars,
|
||||
package_download_count: pulseMCPItem.package_download_count,
|
||||
config: mcpProvider.config,
|
||||
tools: mcpProvider.tools,
|
||||
pulseMCP_meta: pulseMCPItem._meta,
|
||||
}),
|
||||
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: string;
|
||||
const queryParams: any[] = [];
|
||||
|
||||
if (params.query) {
|
||||
// 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}`;
|
||||
}
|
||||
|
||||
// Paginação
|
||||
if (params.limit) {
|
||||
query += " LIMIT ? OFFSET ?";
|
||||
queryParams.push(params.limit, params.offset || 0);
|
||||
}
|
||||
|
||||
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: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-33
@@ -44,34 +44,7 @@ CREATE TABLE IF NOT EXISTS mcp_installations (
|
||||
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
|
||||
);
|
||||
-- Marketplace removido: tabelas e índices relacionados ao cache do marketplace foram descontinuados
|
||||
|
||||
-- Tabela para rastrear MCPs ativos por conversa
|
||||
CREATE TABLE IF NOT EXISTS active_mcps (
|
||||
@@ -93,8 +66,4 @@ 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);
|
||||
-- Índices do marketplace removidos
|
||||
+31
-2
@@ -9,20 +9,49 @@ export async function generateEmbedding(
|
||||
model: string = "nomic-embed-text"
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
// Verificar se deve forçar uso de CPU (útil quando GPU tem problemas de memória)
|
||||
const forceCpu =
|
||||
process.env.OLLAMA_NO_GPU === "1" || process.env.OLLAMA_NUM_GPU === "0";
|
||||
|
||||
const response = await ollama.embeddings({
|
||||
model,
|
||||
prompt: text,
|
||||
options: forceCpu ? { num_gpu: 0 } : undefined,
|
||||
});
|
||||
|
||||
return new Float32Array(response.embedding);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao gerar embedding:", error);
|
||||
|
||||
// Se erro de GPU e ainda não tentou CPU, tentar forçar CPU
|
||||
const isGpuError =
|
||||
error?.message?.includes("CUDA") ||
|
||||
error?.message?.includes("unable to allocate") ||
|
||||
error?.status_code === 500;
|
||||
|
||||
if (
|
||||
isGpuError &&
|
||||
process.env.OLLAMA_NO_GPU !== "1" &&
|
||||
process.env.OLLAMA_NUM_GPU !== "0"
|
||||
) {
|
||||
console.warn("Erro de GPU detectado, tentando usar CPU...");
|
||||
try {
|
||||
const response = await ollama.embeddings({
|
||||
model,
|
||||
prompt: text,
|
||||
options: { num_gpu: 0 },
|
||||
});
|
||||
return new Float32Array(response.embedding);
|
||||
} catch (cpuError) {
|
||||
console.error("Erro mesmo usando CPU:", cpuError);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: retornar vetor zero se modelo não estiver disponível
|
||||
console.warn(
|
||||
`Modelo ${model} não disponível, usando vetor zero como fallback`
|
||||
);
|
||||
return new Float32Array(384); // 384 dimensões padrão
|
||||
return new Float32Array(768); // 768 dimensões para nomic-embed-text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ export function extractToolCallFromResponse(response: string): {
|
||||
|
||||
const parsed = JSON.parse(jsonText.trim());
|
||||
|
||||
// Formato atual suportado pelo prompt engineering
|
||||
if (parsed.tool_call && parsed.tool_call.name) {
|
||||
return {
|
||||
name: parsed.tool_call.name,
|
||||
@@ -157,14 +158,60 @@ export function extractToolCallFromResponse(response: string): {
|
||||
};
|
||||
}
|
||||
|
||||
// Formato OpenAI-like: { type: 'function', function: { name, parameters|arguments } }
|
||||
// Algumas implementações usam `arguments` como string JSON
|
||||
if (parsed.type === "function" && parsed.function && parsed.function.name) {
|
||||
const fn = parsed.function;
|
||||
let params: any = fn.parameters ?? fn.arguments ?? {};
|
||||
if (typeof params === "string") {
|
||||
try {
|
||||
params = JSON.parse(params);
|
||||
} catch {
|
||||
// se não for JSON válido, mantém string (executor fará fallback)
|
||||
}
|
||||
}
|
||||
return { name: fn.name, parameters: params || {} };
|
||||
}
|
||||
|
||||
// Formato simples: { name: "tool", parameters: {...} }
|
||||
if (parsed.name && (parsed.parameters || parsed.arguments)) {
|
||||
let params: any = parsed.parameters ?? parsed.arguments ?? {};
|
||||
if (typeof params === "string") {
|
||||
try {
|
||||
params = JSON.parse(params);
|
||||
} catch {
|
||||
// mantém string se não for JSON válido
|
||||
}
|
||||
}
|
||||
return { name: parsed.name, parameters: params || {} };
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
// Tentar extrair manualmente
|
||||
const nameMatch = response.match(/"name":\s*"([^"]+)"/);
|
||||
if (nameMatch) {
|
||||
const name = nameMatch[1];
|
||||
const paramsMatch = response.match(/"parameters":\s*({[^}]+})/);
|
||||
const parameters = paramsMatch ? JSON.parse(paramsMatch[1]) : {};
|
||||
// Captura tanto parameters quanto arguments
|
||||
const paramsMatch =
|
||||
response.match(/"parameters"\s*:\s*({[\s\S]*?})/) ||
|
||||
response.match(/"arguments"\s*:\s*("[\s\S]*?"|{[\s\S]*?})/);
|
||||
let parameters: any = {};
|
||||
if (paramsMatch) {
|
||||
const raw = paramsMatch[1];
|
||||
try {
|
||||
parameters = JSON.parse(raw);
|
||||
} catch {
|
||||
// caso venha como string com JSON dentro de aspas
|
||||
if (raw.startsWith('"') && raw.endsWith('"')) {
|
||||
try {
|
||||
parameters = JSON.parse(raw.slice(1, -1));
|
||||
} catch {
|
||||
parameters = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { name, parameters };
|
||||
}
|
||||
|
||||
@@ -31,16 +31,31 @@ export class MCPExecutor {
|
||||
}
|
||||
|
||||
// Verificar se está pronto
|
||||
if (mcpData.status !== "ready") {
|
||||
// Se status é undefined/null, considerar como "ready" (MCPs instalados antes da implementação de status)
|
||||
if (mcpData.status && mcpData.status !== "ready") {
|
||||
throw new Error(
|
||||
`MCP ${mcpId} is not ready (status: ${mcpData.status})`
|
||||
);
|
||||
}
|
||||
|
||||
// Obter comando executável
|
||||
const config = JSON.parse(mcpData.config);
|
||||
const command = config.command || mcpData.executable_command;
|
||||
const args = config.args || [];
|
||||
// mcpData.config já vem parseado do repositório, mas pode ser string em casos legados
|
||||
const config =
|
||||
typeof mcpData.config === "string"
|
||||
? JSON.parse(mcpData.config)
|
||||
: mcpData.config || {};
|
||||
const commandFromConfig = config.command;
|
||||
const argsFromConfig = Array.isArray(config.args) ? config.args : [];
|
||||
|
||||
let command = commandFromConfig;
|
||||
let args = argsFromConfig;
|
||||
|
||||
// Se não houver command em config, reconstruir a partir de executable_command (se existir)
|
||||
if (!command && mcpData.executable_command) {
|
||||
const parts = String(mcpData.executable_command).trim().split(/\s+/);
|
||||
command = parts[0];
|
||||
args = parts.slice(1);
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
throw new Error(`No executable command found for MCP ${mcpId}`);
|
||||
@@ -100,14 +115,14 @@ export class MCPExecutor {
|
||||
): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const env = {
|
||||
...process.env,
|
||||
...globalThis.process.env,
|
||||
...(environmentPath && {
|
||||
PATH: `${environmentPath}/node_modules/.bin:${process.env.PATH}`,
|
||||
PATH: `${environmentPath}/node_modules/.bin:${globalThis.process.env.PATH}`,
|
||||
}),
|
||||
};
|
||||
|
||||
const process = spawn(command, args, {
|
||||
cwd: environmentPath || process.cwd(),
|
||||
const childProcess = spawn(command, args, {
|
||||
cwd: environmentPath || globalThis.process.cwd(),
|
||||
env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
@@ -119,26 +134,35 @@ export class MCPExecutor {
|
||||
// Timeout
|
||||
const timeout = setTimeout(() => {
|
||||
if (!responseReceived) {
|
||||
process.kill();
|
||||
childProcess.kill();
|
||||
reject(new Error(`Execution timeout after ${EXECUTION_TIMEOUT}ms`));
|
||||
}
|
||||
}, EXECUTION_TIMEOUT);
|
||||
|
||||
// Capturar stdout
|
||||
process.stdout?.on("data", (data) => {
|
||||
childProcess.stdout?.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
// Loga por linhas para diagnóstico
|
||||
const printedLines = data
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter((l: string) => l.trim().length > 0);
|
||||
for (const l of printedLines) {
|
||||
console.log(`📥 [MCP stdout] ${l}`);
|
||||
}
|
||||
|
||||
// Tentar parsear respostas JSON-RPC
|
||||
const lines = stdout.split("\n");
|
||||
for (const line of lines) {
|
||||
const parsedLines = stdout.split("\n");
|
||||
for (const line of parsedLines) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
console.log("🔎 [MCP stdout parsed]", parsed);
|
||||
if (parsed.id === 2 && parsed.result) {
|
||||
// Resposta do tools/call
|
||||
clearTimeout(timeout);
|
||||
responseReceived = true;
|
||||
process.kill();
|
||||
childProcess.kill();
|
||||
resolve(parsed.result);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -149,18 +173,25 @@ export class MCPExecutor {
|
||||
});
|
||||
|
||||
// Capturar stderr
|
||||
process.stderr?.on("data", (data) => {
|
||||
childProcess.stderr?.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
const errLines = data
|
||||
.toString()
|
||||
.split("\n")
|
||||
.filter((l: string) => l.trim().length > 0);
|
||||
for (const l of errLines) {
|
||||
console.warn(`⚠️ [MCP stderr] ${l}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Erro de processo
|
||||
process.on("error", (error) => {
|
||||
childProcess.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`Process error: ${error.message}`));
|
||||
});
|
||||
|
||||
// Processo encerrou
|
||||
process.on("close", (code) => {
|
||||
childProcess.on("close", (code) => {
|
||||
clearTimeout(timeout);
|
||||
if (!responseReceived) {
|
||||
if (code !== 0) {
|
||||
@@ -186,7 +217,8 @@ export class MCPExecutor {
|
||||
},
|
||||
id: 1,
|
||||
};
|
||||
process.stdin?.write(JSON.stringify(initRequest) + "\n");
|
||||
console.log("📤 [MCP send] initialize", initRequest);
|
||||
childProcess.stdin?.write(JSON.stringify(initRequest) + "\n");
|
||||
|
||||
// Aguardar um pouco antes de enviar tool call
|
||||
setTimeout(() => {
|
||||
@@ -200,12 +232,13 @@ export class MCPExecutor {
|
||||
},
|
||||
id: 2,
|
||||
};
|
||||
process.stdin?.write(JSON.stringify(toolCallRequest) + "\n");
|
||||
process.stdin?.end();
|
||||
console.log("📤 [MCP send] tools/call", toolCallRequest);
|
||||
childProcess.stdin?.write(JSON.stringify(toolCallRequest) + "\n");
|
||||
childProcess.stdin?.end();
|
||||
}, 500);
|
||||
} catch (error: any) {
|
||||
clearTimeout(timeout);
|
||||
process.kill();
|
||||
childProcess.kill();
|
||||
reject(new Error(`Failed to send JSON-RPC request: ${error.message}`));
|
||||
}
|
||||
});
|
||||
@@ -274,6 +307,16 @@ export class MCPExecutor {
|
||||
|
||||
if (expectedType) {
|
||||
const actualType = typeof value;
|
||||
// Tratar objeto vazio como "ausente" quando tipo esperado não é object
|
||||
if (
|
||||
value &&
|
||||
actualType === "object" &&
|
||||
!Array.isArray(value) &&
|
||||
Object.keys(value).length === 0 &&
|
||||
expectedType !== "object"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const valid =
|
||||
(expectedType === "string" && actualType === "string") ||
|
||||
(expectedType === "number" && actualType === "number") ||
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
import { PulseMCPService } from "./pulsemcp";
|
||||
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
|
||||
|
||||
export interface SyncProgress {
|
||||
current: number;
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface SyncResult {
|
||||
success: boolean;
|
||||
totalDownloaded: number;
|
||||
errors: string[];
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export class MCPSyncService {
|
||||
private static isSyncingFlag = false;
|
||||
private static syncPromise: Promise<SyncResult> | null = null;
|
||||
private static abortController: AbortController | null = null;
|
||||
private static currentProgress: SyncProgress | null = null;
|
||||
|
||||
/**
|
||||
* Download completo com paginação incremental
|
||||
*/
|
||||
static async syncAll(
|
||||
onProgress?: (progress: SyncProgress) => void
|
||||
): Promise<SyncResult> {
|
||||
// Se já existe um sync em andamento, retornar a promise existente
|
||||
if (this.syncPromise) {
|
||||
console.log("⏳ Sync already in progress, waiting for completion...");
|
||||
return this.syncPromise;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const errors: string[] = [];
|
||||
let totalDownloaded = 0;
|
||||
|
||||
// Criar a promise de sync e armazená-la
|
||||
this.syncPromise = (async () => {
|
||||
try {
|
||||
if (this.isSyncingFlag) {
|
||||
throw new Error("Sync already in progress");
|
||||
}
|
||||
|
||||
this.isSyncingFlag = true;
|
||||
this.abortController = new AbortController();
|
||||
|
||||
console.log("🔄 Starting MCP marketplace sync...");
|
||||
|
||||
// Limpar cache antigo antes de começar
|
||||
MCPCacheRepository.clearCache();
|
||||
|
||||
const allMCPs = await this.downloadAllPages(onProgress);
|
||||
totalDownloaded = allMCPs.length;
|
||||
|
||||
// Salvar todos os MCPs no banco
|
||||
if (allMCPs.length > 0) {
|
||||
MCPCacheRepository.saveBatch(allMCPs);
|
||||
|
||||
// Atualizar metadados
|
||||
MCPCacheRepository.updateMetadata("last_sync", Date.now().toString());
|
||||
MCPCacheRepository.updateMetadata(
|
||||
"total_items",
|
||||
allMCPs.length.toString()
|
||||
);
|
||||
|
||||
console.log(`✅ Sync completed: ${totalDownloaded} MCPs downloaded`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
success: true,
|
||||
totalDownloaded,
|
||||
errors,
|
||||
duration,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ Sync failed:", error);
|
||||
errors.push(error.message);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
return {
|
||||
success: false,
|
||||
totalDownloaded,
|
||||
errors,
|
||||
duration,
|
||||
};
|
||||
} finally {
|
||||
this.isSyncingFlag = false;
|
||||
this.abortController = null;
|
||||
this.currentProgress = null;
|
||||
this.syncPromise = null; // Limpar a promise compartilhada
|
||||
}
|
||||
})();
|
||||
|
||||
return this.syncPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sincronização em background (usado automaticamente)
|
||||
*/
|
||||
static async backgroundSync(): Promise<void> {
|
||||
try {
|
||||
if (this.isSyncingFlag) {
|
||||
console.log("Sync already in progress, skipping background sync");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🔄 Starting background MCP sync...");
|
||||
await this.syncAll((progress) => {
|
||||
console.log(
|
||||
`📥 Downloaded ${progress.current}/${progress.total} MCPs (page ${progress.page}/${progress.totalPages})`
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Background sync failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar se sincronização está em andamento
|
||||
*/
|
||||
static isSyncing(): boolean {
|
||||
return this.isSyncingFlag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancelar sincronização atual
|
||||
*/
|
||||
static cancelSync(): void {
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
console.log("🛑 Sync cancelled by user");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter progresso atual da sincronização
|
||||
*/
|
||||
static getCurrentProgress(): SyncProgress | null {
|
||||
return this.currentProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download completo de todos os servidores PulseMCP (uma única chamada)
|
||||
*/
|
||||
private static async downloadAllPages(
|
||||
onProgress?: (progress: SyncProgress) => void
|
||||
): Promise<any[]> {
|
||||
try {
|
||||
console.log("📡 Starting complete download from PulseMCP...");
|
||||
|
||||
// Notificar início
|
||||
this.currentProgress = {
|
||||
current: 0,
|
||||
total: 0,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(this.currentProgress);
|
||||
}
|
||||
|
||||
// Fazer uma única chamada para buscar todos os servidores
|
||||
const response = await PulseMCPService.getAllServers();
|
||||
|
||||
console.log(`📊 Total servers available: ${response.total_count}`);
|
||||
console.log(`📥 Downloaded ${response.servers.length} servers`);
|
||||
|
||||
// Atualizar progresso
|
||||
this.currentProgress = {
|
||||
current: response.servers.length,
|
||||
total: response.total_count,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(this.currentProgress);
|
||||
}
|
||||
|
||||
// Salvar em batches de 500 para evitar bloqueio
|
||||
const batchSize = 500;
|
||||
const totalBatches = Math.ceil(response.servers.length / batchSize);
|
||||
|
||||
for (let i = 0; i < totalBatches; i++) {
|
||||
const start = i * batchSize;
|
||||
const end = Math.min(start + batchSize, response.servers.length);
|
||||
const batch = response.servers.slice(start, end);
|
||||
|
||||
console.log(
|
||||
`💾 Saving batch ${i + 1}/${totalBatches} (${batch.length} items)...`
|
||||
);
|
||||
MCPCacheRepository.saveBatch(batch);
|
||||
|
||||
// Pequena pausa entre batches
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ Download completed: ${response.servers.length} total MCPs downloaded from PulseMCP`
|
||||
);
|
||||
return response.servers;
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Error downloading from PulseMCP:`, error);
|
||||
|
||||
// Se for erro de abort, parar
|
||||
if (this.abortController?.signal.aborted) {
|
||||
console.log("🛑 Download cancelled");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Para outros erros, lançar
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar se precisa sincronizar
|
||||
*/
|
||||
static needsSync(): boolean {
|
||||
return MCPCacheRepository.needsSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter estatísticas do cache
|
||||
*/
|
||||
static getCacheStats() {
|
||||
return MCPCacheRepository.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forçar limpeza do cache
|
||||
*/
|
||||
static clearCache(): void {
|
||||
MCPCacheRepository.clearCache();
|
||||
console.log("🗑️ Cache cleared");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar saúde do sistema de cache
|
||||
*/
|
||||
static async healthCheck(): Promise<{
|
||||
cacheExists: boolean;
|
||||
cacheValid: boolean;
|
||||
totalItems: number;
|
||||
lastSync: number | null;
|
||||
needsSync: boolean;
|
||||
}> {
|
||||
try {
|
||||
const cacheExists = MCPCacheRepository.tableExists(
|
||||
"mcp_marketplace_cache"
|
||||
);
|
||||
const stats = MCPCacheRepository.getStats();
|
||||
const needsSync = MCPCacheRepository.needsSync();
|
||||
|
||||
return {
|
||||
cacheExists,
|
||||
cacheValid: stats.isValid,
|
||||
totalItems: stats.totalItems,
|
||||
lastSync: stats.lastSync,
|
||||
needsSync,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Health check failed:", error);
|
||||
return {
|
||||
cacheExists: false,
|
||||
cacheValid: false,
|
||||
totalItems: 0,
|
||||
lastSync: null,
|
||||
needsSync: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,462 +0,0 @@
|
||||
import {
|
||||
PulseMCPSearchParams,
|
||||
PulseMCPResponse,
|
||||
MCPServerMetadata,
|
||||
MCPProvider,
|
||||
} from "@/lib/types/mcp";
|
||||
import { MCPCache } from "@/lib/cache/mcp-cache";
|
||||
|
||||
const PULSEMCP_API_BASE = "https://api.pulsemcp.com/v0beta";
|
||||
|
||||
export class PulseMCPService {
|
||||
// Mapear categorias do PulseMCP para categorias internas
|
||||
private static categoryMap: Record<string, MCPProvider["category"]> = {
|
||||
filesystem: "other",
|
||||
github: "code",
|
||||
google: "search",
|
||||
brave: "search",
|
||||
postgres: "database",
|
||||
sqlite: "database",
|
||||
mongodb: "database",
|
||||
redis: "database",
|
||||
aws: "other",
|
||||
gcp: "other",
|
||||
azure: "other",
|
||||
slack: "other",
|
||||
discord: "other",
|
||||
notion: "office",
|
||||
confluence: "office",
|
||||
jira: "office",
|
||||
figma: "other",
|
||||
linear: "office",
|
||||
trello: "office",
|
||||
calendar: "office",
|
||||
email: "office",
|
||||
maps: "map",
|
||||
weather: "other",
|
||||
news: "other",
|
||||
finance: "finance",
|
||||
crypto: "finance",
|
||||
stocks: "finance",
|
||||
payment: "payment",
|
||||
stripe: "payment",
|
||||
paypal: "payment",
|
||||
chart: "chart",
|
||||
analytics: "chart",
|
||||
monitoring: "other",
|
||||
logging: "other",
|
||||
security: "other",
|
||||
auth: "other",
|
||||
api: "other",
|
||||
webhook: "other",
|
||||
scraping: "other",
|
||||
automation: "other",
|
||||
ai: "other",
|
||||
ml: "other",
|
||||
data: "other",
|
||||
etl: "other",
|
||||
visualization: "chart",
|
||||
reporting: "chart",
|
||||
};
|
||||
|
||||
/**
|
||||
* Buscar todos os servidores MCP na API PulseMCP (sem parâmetros)
|
||||
* Com retry automático e backoff exponencial para rate limiting
|
||||
*/
|
||||
static async getAllServers(retryCount = 0): Promise<PulseMCPResponse> {
|
||||
const maxRetries = 3;
|
||||
const baseDelay = 2000; // 2 segundos base
|
||||
|
||||
try {
|
||||
if (retryCount > 0) {
|
||||
console.log(
|
||||
`📡 Retry ${retryCount}/${maxRetries} - Fetching from PulseMCP API...`
|
||||
);
|
||||
} else {
|
||||
console.log("📡 Fetching all servers from PulseMCP API...");
|
||||
}
|
||||
|
||||
const response = await fetch(`${PULSEMCP_API_BASE}/servers`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0 (https://ollahub.com)",
|
||||
},
|
||||
// Timeout de 30 segundos para download completo
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Se for 429 (Too Many Requests), tentar novamente com backoff
|
||||
if (response.status === 429 && retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount); // Backoff exponencial
|
||||
console.warn(
|
||||
`⚠️ Rate limit hit (429). Waiting ${delay}ms before retry ${
|
||||
retryCount + 1
|
||||
}/${maxRetries}...`
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return this.getAllServers(retryCount + 1);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`PulseMCP API error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(
|
||||
`✅ PulseMCP API returned ${data.servers.length} servers (total: ${data.total_count})`
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
// Se for erro de rate limit e ainda temos retries, tentar novamente
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("429") &&
|
||||
retryCount < maxRetries
|
||||
) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount);
|
||||
console.warn(
|
||||
`⚠️ Rate limit error. Waiting ${delay}ms before retry ${
|
||||
retryCount + 1
|
||||
}/${maxRetries}...`
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return this.getAllServers(retryCount + 1);
|
||||
}
|
||||
|
||||
console.error("❌ Error calling PulseMCP API:", error);
|
||||
throw new Error(
|
||||
`Failed to fetch MCPs after ${retryCount + 1} attempts: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformar resposta PulseMCP para formato interno MCPProvider
|
||||
*/
|
||||
static transformToMCPProvider(pulseMCPItem: MCPServerMetadata): MCPProvider {
|
||||
const meta = pulseMCPItem._meta?.["com.pulsemcp"];
|
||||
const category = this.mapCategory(
|
||||
pulseMCPItem.name,
|
||||
pulseMCPItem.short_description
|
||||
);
|
||||
|
||||
// Usar name como ID se package_name não estiver disponível
|
||||
const id =
|
||||
pulseMCPItem.package_name ||
|
||||
pulseMCPItem.name.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
||||
|
||||
return {
|
||||
id: id,
|
||||
name: meta?.standardized_name || pulseMCPItem.name,
|
||||
author: meta?.standardized_provider_name || "Unknown",
|
||||
description:
|
||||
meta?.standardized_description || pulseMCPItem.short_description,
|
||||
version: "1.0.0", // PulseMCP não fornece versão específica
|
||||
category,
|
||||
tags: this.extractTags(pulseMCPItem.name, pulseMCPItem.short_description),
|
||||
rating: this.calculateRating(pulseMCPItem),
|
||||
totalRatings: pulseMCPItem.github_stars || 0,
|
||||
repository: pulseMCPItem.source_code_url,
|
||||
homepage: pulseMCPItem.external_url || pulseMCPItem.source_code_url,
|
||||
installed: false, // Será verificado localmente
|
||||
subfield: category.toUpperCase(),
|
||||
field: "MCP SERVER",
|
||||
config: this.generateConfigTemplate(pulseMCPItem),
|
||||
tools: [], // PulseMCP não fornece schemas de ferramentas
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapear categoria baseada no nome e descrição
|
||||
*/
|
||||
private static mapCategory(
|
||||
name: string,
|
||||
description: string
|
||||
): MCPProvider["category"] {
|
||||
const text = `${name} ${description}`.toLowerCase();
|
||||
|
||||
// Buscar por palavras-chave específicas
|
||||
for (const [keyword, category] of Object.entries(this.categoryMap)) {
|
||||
if (text.includes(keyword)) {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
return "other";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrair tags baseadas no nome e descrição
|
||||
*/
|
||||
private static extractTags(name: string, description: string): string[] {
|
||||
const text = `${name} ${description}`.toLowerCase();
|
||||
const tags: string[] = [];
|
||||
|
||||
// Tags comuns
|
||||
const commonTags = [
|
||||
"api",
|
||||
"database",
|
||||
"filesystem",
|
||||
"github",
|
||||
"google",
|
||||
"aws",
|
||||
"gcp",
|
||||
"azure",
|
||||
"slack",
|
||||
"discord",
|
||||
"notion",
|
||||
"calendar",
|
||||
"email",
|
||||
"maps",
|
||||
"weather",
|
||||
"finance",
|
||||
"crypto",
|
||||
"payment",
|
||||
"chart",
|
||||
"analytics",
|
||||
"monitoring",
|
||||
"security",
|
||||
"auth",
|
||||
"webhook",
|
||||
"scraping",
|
||||
"automation",
|
||||
"ai",
|
||||
"ml",
|
||||
];
|
||||
|
||||
for (const tag of commonTags) {
|
||||
if (text.includes(tag)) {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return tags.slice(0, 5); // Limitar a 5 tags
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcular rating baseado em métricas disponíveis
|
||||
*/
|
||||
private static calculateRating(item: MCPServerMetadata): number {
|
||||
const meta = item._meta?.["com.pulsemcp"];
|
||||
let rating = 3.0; // Rating base
|
||||
|
||||
// Ajustar baseado em downloads
|
||||
if (meta?.estimated_downloads_all_time) {
|
||||
const downloads = meta.estimated_downloads_all_time;
|
||||
if (downloads > 1000000) rating += 1.0;
|
||||
else if (downloads > 100000) rating += 0.5;
|
||||
else if (downloads > 10000) rating += 0.2;
|
||||
}
|
||||
|
||||
// Ajustar baseado em GitHub stars
|
||||
if (item.github_stars) {
|
||||
if (item.github_stars > 1000) rating += 0.5;
|
||||
else if (item.github_stars > 100) rating += 0.2;
|
||||
}
|
||||
|
||||
// Ajustar se é oficial
|
||||
if (meta?.is_official) {
|
||||
rating += 0.5;
|
||||
}
|
||||
|
||||
return Math.min(5.0, Math.max(1.0, rating));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrair capacidades baseadas no nome e descrição
|
||||
*/
|
||||
private static extractCapabilities(item: MCPServerMetadata): string[] {
|
||||
const text = `${item.name} ${item.short_description}`.toLowerCase();
|
||||
const capabilities: string[] = [];
|
||||
|
||||
// Capacidades comuns
|
||||
const capabilityMap = {
|
||||
read: ["read", "get", "fetch", "retrieve"],
|
||||
write: ["write", "create", "update", "modify", "edit"],
|
||||
delete: ["delete", "remove", "destroy"],
|
||||
search: ["search", "find", "query", "filter"],
|
||||
sync: ["sync", "synchronize", "backup"],
|
||||
monitor: ["monitor", "watch", "track", "observe"],
|
||||
authenticate: ["auth", "login", "authenticate"],
|
||||
notify: ["notify", "alert", "message", "send"],
|
||||
};
|
||||
|
||||
for (const [capability, keywords] of Object.entries(capabilityMap)) {
|
||||
if (keywords.some((keyword) => text.includes(keyword))) {
|
||||
capabilities.push(capability);
|
||||
}
|
||||
}
|
||||
|
||||
return capabilities.slice(0, 5); // Limitar a 5 capacidades
|
||||
}
|
||||
|
||||
/**
|
||||
* Gerar template de configuração para o servidor
|
||||
*/
|
||||
private static generateConfigTemplate(item: MCPServerMetadata): any[] {
|
||||
const config: any[] = [];
|
||||
|
||||
// Configuração básica
|
||||
config.push({
|
||||
type: "server",
|
||||
name: (item.package_name || item.name).replace(/^@[\w-]+\//, ""),
|
||||
command: this.getCommand(item.package_registry),
|
||||
args: this.getArgs(item),
|
||||
});
|
||||
|
||||
// Adicionar variáveis de ambiente se necessário
|
||||
const envVars = this.getEnvTemplate(item);
|
||||
if (Object.keys(envVars).length > 0) {
|
||||
config.push({
|
||||
type: "env",
|
||||
variables: envVars,
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter comando baseado no registry
|
||||
*/
|
||||
private static getCommand(registry: string | null): string {
|
||||
switch (registry) {
|
||||
case "npm":
|
||||
return "npx";
|
||||
case "pypi":
|
||||
return "python";
|
||||
case "cargo":
|
||||
return "cargo";
|
||||
default:
|
||||
return "node";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter argumentos baseados no registry
|
||||
*/
|
||||
private static getArgs(item: MCPServerMetadata): string[] {
|
||||
switch (item.package_registry) {
|
||||
case "npm":
|
||||
return ["-y", item.package_name || item.name];
|
||||
case "pypi":
|
||||
return ["-m", item.package_name || item.name];
|
||||
case "cargo":
|
||||
return ["run", "--bin", item.package_name || item.name];
|
||||
default:
|
||||
return [item.package_name || item.name];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter template de variáveis de ambiente
|
||||
*/
|
||||
private static getEnvTemplate(
|
||||
item: MCPServerMetadata
|
||||
): Record<string, string> {
|
||||
const template: Record<string, string> = {};
|
||||
const text = `${item.name} ${item.short_description}`.toLowerCase();
|
||||
|
||||
// Templates comuns baseados em palavras-chave
|
||||
if (text.includes("google")) {
|
||||
template.GOOGLE_API_KEY = "your-google-api-key";
|
||||
}
|
||||
if (text.includes("github")) {
|
||||
template.GITHUB_TOKEN = "your-github-token";
|
||||
}
|
||||
if (text.includes("openai")) {
|
||||
template.OPENAI_API_KEY = "your-openai-api-key";
|
||||
}
|
||||
if (text.includes("aws")) {
|
||||
template.AWS_ACCESS_KEY_ID = "your-aws-access-key";
|
||||
template.AWS_SECRET_ACCESS_KEY = "your-aws-secret-key";
|
||||
}
|
||||
if (text.includes("slack")) {
|
||||
template.SLACK_BOT_TOKEN = "your-slack-bot-token";
|
||||
}
|
||||
if (text.includes("discord")) {
|
||||
template.DISCORD_TOKEN = "your-discord-token";
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar se a API está disponível
|
||||
*/
|
||||
static async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${PULSEMCP_API_BASE}/servers?count_per_page=1`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0 (https://ollahub.com)",
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error("PulseMCP API health check failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter servidores populares
|
||||
*/
|
||||
static async getPopularServers(
|
||||
limit: number = 10
|
||||
): Promise<MCPServerMetadata[]> {
|
||||
try {
|
||||
const response = await this.getAllServers();
|
||||
|
||||
return response.servers
|
||||
.sort((a: MCPServerMetadata, b: MCPServerMetadata) => {
|
||||
const aDownloads =
|
||||
a._meta?.["com.pulsemcp"]?.estimated_downloads_all_time || 0;
|
||||
const bDownloads =
|
||||
b._meta?.["com.pulsemcp"]?.estimated_downloads_all_time || 0;
|
||||
return bDownloads - aDownloads;
|
||||
})
|
||||
.slice(0, limit);
|
||||
} catch (error) {
|
||||
console.error("Error getting popular servers:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buscar servidores por integração específica
|
||||
*/
|
||||
static async getServersByIntegration(
|
||||
integration: string
|
||||
): Promise<MCPServerMetadata[]> {
|
||||
try {
|
||||
const response = await this.getAllServers();
|
||||
// Filtrar servidores que contenham a integração no nome ou descrição
|
||||
return response.servers.filter(
|
||||
(server) =>
|
||||
server.name.toLowerCase().includes(integration.toLowerCase()) ||
|
||||
server.short_description
|
||||
.toLowerCase()
|
||||
.includes(integration.toLowerCase())
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error getting servers for integration ${integration}:`,
|
||||
error
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-70
@@ -54,51 +54,7 @@ export interface MCPTool {
|
||||
};
|
||||
}
|
||||
|
||||
export interface DeepNLPSearchParams {
|
||||
query?: string;
|
||||
category?: string;
|
||||
id?: string;
|
||||
page_id?: number;
|
||||
count_per_page?: number;
|
||||
offset?: number;
|
||||
mode?: "list" | "dict";
|
||||
}
|
||||
|
||||
export interface DeepNLPResponse {
|
||||
query?: string;
|
||||
items: any[];
|
||||
count: number;
|
||||
total_hits: number;
|
||||
}
|
||||
|
||||
export interface MCPServerConfig {
|
||||
total_hits: number;
|
||||
id: string;
|
||||
items: any[];
|
||||
}
|
||||
|
||||
export interface MCPSearchParams {
|
||||
category?: string;
|
||||
search?: string;
|
||||
sort?: "rating" | "name" | "recent" | "total_ratings" | "updated_at";
|
||||
order?: "asc" | "desc";
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
// PulseMCP API Types
|
||||
export interface PulseMCPSearchParams {
|
||||
query?: string;
|
||||
integrations?: string[];
|
||||
count_per_page?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface PulseMCPResponse {
|
||||
servers: MCPServerMetadata[];
|
||||
next?: string;
|
||||
total_count: number;
|
||||
}
|
||||
// Tipos relacionados a busca/marketplace foram removidos
|
||||
|
||||
export interface MCPTestResult {
|
||||
success: boolean;
|
||||
@@ -113,28 +69,4 @@ export interface MCPTestRequest {
|
||||
mcpId: string;
|
||||
}
|
||||
|
||||
export interface MCPServerMetadata {
|
||||
name: string;
|
||||
url: string;
|
||||
external_url?: string;
|
||||
short_description: string;
|
||||
source_code_url: string;
|
||||
github_stars?: number;
|
||||
package_registry: "npm" | "pypi" | "cargo";
|
||||
package_name: string;
|
||||
package_download_count?: number;
|
||||
EXPERIMENTAL_ai_generated_description?: string;
|
||||
_meta: {
|
||||
"com.pulsemcp": {
|
||||
estimated_downloads_all_time: number;
|
||||
estimated_downloads_last_30_days: number;
|
||||
estimated_downloads_last_7_days: number;
|
||||
standardized_name: string;
|
||||
standardized_description: string;
|
||||
standardized_provider_name: string;
|
||||
standardized_provider_url: string;
|
||||
estimated_released_on: string;
|
||||
is_official: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
// Metadados específicos de PulseMCP removidos
|
||||
@@ -1,66 +0,0 @@
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
|
||||
const dbPath = path.join(process.cwd(), "database", "ollahub.db");
|
||||
const db = new Database(dbPath);
|
||||
|
||||
console.log("🔧 Adding FTS5 support to MCP cache...");
|
||||
|
||||
// Criar tabela FTS5
|
||||
db.exec(`
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS mcp_search_fts USING fts5(
|
||||
id UNINDEXED,
|
||||
content_name,
|
||||
description,
|
||||
content_tag_list,
|
||||
publisher_id,
|
||||
content=mcp_marketplace_cache,
|
||||
content_rowid=rowid
|
||||
);
|
||||
`);
|
||||
|
||||
// Popular com dados existentes
|
||||
console.log("📦 Populating FTS index...");
|
||||
db.exec(`
|
||||
INSERT INTO mcp_search_fts(rowid, id, content_name, description, content_tag_list, publisher_id)
|
||||
SELECT rowid, id, content_name, description, content_tag_list, publisher_id
|
||||
FROM mcp_marketplace_cache;
|
||||
`);
|
||||
|
||||
// Criar triggers
|
||||
console.log("⚡ Creating triggers...");
|
||||
db.exec(`
|
||||
CREATE TRIGGER IF NOT EXISTS mcp_fts_insert AFTER INSERT ON mcp_marketplace_cache BEGIN
|
||||
INSERT INTO mcp_search_fts(rowid, id, content_name, description, content_tag_list, publisher_id)
|
||||
VALUES (new.rowid, new.id, new.content_name, new.description, new.content_tag_list, new.publisher_id);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS mcp_fts_update AFTER UPDATE ON mcp_marketplace_cache BEGIN
|
||||
UPDATE mcp_search_fts
|
||||
SET content_name = new.content_name,
|
||||
description = new.description,
|
||||
content_tag_list = new.content_tag_list,
|
||||
publisher_id = new.publisher_id
|
||||
WHERE rowid = old.rowid;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS mcp_fts_delete AFTER DELETE ON mcp_marketplace_cache BEGIN
|
||||
DELETE FROM mcp_search_fts WHERE rowid = old.rowid;
|
||||
END;
|
||||
`);
|
||||
|
||||
// Criar índices compostos
|
||||
console.log("🔍 Creating composite indexes...");
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_cache_category_rating
|
||||
ON mcp_marketplace_cache(category, rating DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_cache_subfield_rating
|
||||
ON mcp_marketplace_cache(subfield, rating DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_cache_updated_rating
|
||||
ON mcp_marketplace_cache(updated_at DESC, rating DESC);
|
||||
`);
|
||||
|
||||
console.log("✅ FTS5 migration completed!");
|
||||
db.close();
|
||||
@@ -1,187 +0,0 @@
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
|
||||
const dbPath = path.join(process.cwd(), "database", "ollahub.db");
|
||||
const db = new Database(dbPath);
|
||||
|
||||
console.log("🚀 Benchmark de Performance - MCP Marketplace");
|
||||
console.log("=".repeat(50));
|
||||
|
||||
// Função para medir tempo de execução
|
||||
function measureTime<T>(name: string, fn: () => T): T {
|
||||
const start = performance.now();
|
||||
const result = fn();
|
||||
const end = performance.now();
|
||||
console.log(`⏱️ ${name}: ${(end - start).toFixed(2)}ms`);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Benchmark 1: Busca textual com FTS5
|
||||
console.log("\n📊 BENCHMARK 1: Busca Textual");
|
||||
console.log("-".repeat(30));
|
||||
|
||||
const searchTerms = ["context", "search", "browser", "map", "ai"];
|
||||
|
||||
searchTerms.forEach((term) => {
|
||||
// Busca com FTS5 (nova implementação)
|
||||
const ftsResults = measureTime(`FTS5 "${term}"`, () => {
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.* FROM mcp_marketplace_cache c
|
||||
INNER JOIN mcp_search_fts f ON c.rowid = f.rowid
|
||||
WHERE mcp_search_fts MATCH ?
|
||||
ORDER BY f.rank, c.rating DESC
|
||||
LIMIT 50
|
||||
`);
|
||||
return stmt.all(`${term}*`);
|
||||
});
|
||||
|
||||
// Busca com LIKE (implementação antiga)
|
||||
const likeResults = measureTime(`LIKE "${term}"`, () => {
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM mcp_marketplace_cache
|
||||
WHERE content_name LIKE ? OR description LIKE ? OR content_tag_list LIKE ?
|
||||
ORDER BY rating DESC
|
||||
LIMIT 50
|
||||
`);
|
||||
const searchPattern = `%${term}%`;
|
||||
return stmt.all(searchPattern, searchPattern, searchPattern);
|
||||
});
|
||||
|
||||
console.log(` 📈 Resultados FTS5: ${ftsResults.length}`);
|
||||
console.log(` 📈 Resultados LIKE: ${likeResults.length}`);
|
||||
});
|
||||
|
||||
// Benchmark 2: Queries com índices compostos
|
||||
console.log("\n📊 BENCHMARK 2: Queries com Índices Compostos");
|
||||
console.log("-".repeat(30));
|
||||
|
||||
const categories = ["search", "browser", "map", "ai"];
|
||||
|
||||
categories.forEach((category) => {
|
||||
// Query com índice composto (nova implementação)
|
||||
const indexedResults = measureTime(`Índice composto "${category}"`, () => {
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM mcp_marketplace_cache
|
||||
WHERE category = ?
|
||||
ORDER BY rating DESC
|
||||
LIMIT 100
|
||||
`);
|
||||
return stmt.all(category);
|
||||
});
|
||||
|
||||
// Query sem índice composto (simulação)
|
||||
const nonIndexedResults = measureTime(`Sem índice "${category}"`, () => {
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM mcp_marketplace_cache
|
||||
WHERE category = ?
|
||||
ORDER BY rating DESC
|
||||
LIMIT 100
|
||||
`);
|
||||
return stmt.all(category);
|
||||
});
|
||||
|
||||
console.log(` 📈 Resultados: ${indexedResults.length}`);
|
||||
});
|
||||
|
||||
// Benchmark 3: Contagem total
|
||||
console.log("\n📊 BENCHMARK 3: Operações de Contagem");
|
||||
console.log("-".repeat(30));
|
||||
|
||||
const totalCount = measureTime("Contagem total", () => {
|
||||
const stmt = db.prepare(
|
||||
"SELECT COUNT(*) as count FROM mcp_marketplace_cache"
|
||||
);
|
||||
return stmt.get() as { count: number };
|
||||
});
|
||||
|
||||
const categoryCount = measureTime("Contagem por categoria", () => {
|
||||
const stmt = db.prepare(`
|
||||
SELECT category, COUNT(*) as count
|
||||
FROM mcp_marketplace_cache
|
||||
GROUP BY category
|
||||
ORDER BY count DESC
|
||||
`);
|
||||
return stmt.all();
|
||||
});
|
||||
|
||||
console.log(` 📈 Total de MCPs: ${totalCount.count}`);
|
||||
console.log(` 📈 Categorias: ${categoryCount.length}`);
|
||||
|
||||
// Benchmark 4: Verificar índices criados
|
||||
console.log("\n📊 BENCHMARK 4: Verificação de Índices");
|
||||
console.log("-".repeat(30));
|
||||
|
||||
const indexes = measureTime("Listar índices", () => {
|
||||
const stmt = db.prepare(`
|
||||
SELECT name, sql FROM sqlite_master
|
||||
WHERE type = 'index' AND name LIKE '%mcp%'
|
||||
ORDER BY name
|
||||
`);
|
||||
return stmt.all();
|
||||
});
|
||||
|
||||
console.log(` 📈 Índices encontrados: ${indexes.length}`);
|
||||
indexes.forEach((idx: any) => {
|
||||
console.log(` - ${idx.name}`);
|
||||
});
|
||||
|
||||
// Benchmark 5: Verificar tabela FTS5
|
||||
console.log("\n📊 BENCHMARK 5: Verificação FTS5");
|
||||
console.log("-".repeat(30));
|
||||
|
||||
const ftsTables = measureTime("Verificar tabelas FTS5", () => {
|
||||
const stmt = db.prepare(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type = 'table' AND name LIKE '%fts%'
|
||||
`);
|
||||
return stmt.all();
|
||||
});
|
||||
|
||||
console.log(` 📈 Tabelas FTS5: ${ftsTables.length}`);
|
||||
ftsTables.forEach((table: any) => {
|
||||
console.log(` - ${table.name}`);
|
||||
});
|
||||
|
||||
// Benchmark 6: Teste de stress
|
||||
console.log("\n📊 BENCHMARK 6: Teste de Stress");
|
||||
console.log("-".repeat(30));
|
||||
|
||||
const stressTest = measureTime("100 buscas simultâneas", () => {
|
||||
const results = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const term = `test${i % 10}`;
|
||||
const stmt = db.prepare(`
|
||||
SELECT c.* FROM mcp_marketplace_cache c
|
||||
INNER JOIN mcp_search_fts f ON c.rowid = f.rowid
|
||||
WHERE mcp_search_fts MATCH ?
|
||||
LIMIT 10
|
||||
`);
|
||||
results.push(stmt.all(`${term}*`));
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
console.log(` 📈 Buscas executadas: ${stressTest.length}`);
|
||||
console.log(
|
||||
` 📈 Total de resultados: ${stressTest.reduce(
|
||||
(sum, r) => sum + r.length,
|
||||
0
|
||||
)}`
|
||||
);
|
||||
|
||||
// Resumo final
|
||||
console.log("\n🎯 RESUMO DOS GANHOS");
|
||||
console.log("=".repeat(50));
|
||||
console.log("✅ FTS5 Full-Text Search implementado");
|
||||
console.log("✅ Índices compostos criados");
|
||||
console.log("✅ React.memo aplicado aos cards");
|
||||
console.log("✅ useMemo para processamento de categorias");
|
||||
console.log("✅ Triggers automáticos para sincronização FTS5");
|
||||
console.log("\n📈 Ganhos esperados:");
|
||||
console.log(" - Busca textual: 10-100x mais rápida");
|
||||
console.log(" - Queries com categoria: 5-15x mais rápidas");
|
||||
console.log(" - Re-renders desnecessários: reduzidos em 80%");
|
||||
console.log(" - Processamento de categorias: otimizado");
|
||||
|
||||
db.close();
|
||||
console.log("\n✅ Benchmark concluído!");
|
||||
@@ -1,90 +0,0 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import { MCPSyncService } from "../lib/services/mcp-sync";
|
||||
import { MCPCacheRepository } from "../database/repositories/mcp-cache";
|
||||
import { PulseMCPService } from "../lib/services/pulsemcp";
|
||||
|
||||
async function forcePulseMCPSync() {
|
||||
console.log("🔄 Forçando sincronização completa com PulseMCP...");
|
||||
|
||||
try {
|
||||
// 1. Verificar se PulseMCP API está funcionando
|
||||
console.log("🔍 Verificando conectividade com PulseMCP API...");
|
||||
const isHealthy = await PulseMCPService.checkHealth();
|
||||
|
||||
if (!isHealthy) {
|
||||
console.error("❌ PulseMCP API não está acessível");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("✅ PulseMCP API está funcionando");
|
||||
|
||||
// 2. Limpar cache antigo completamente
|
||||
console.log("🗑️ Limpando cache antigo do DeepNLP...");
|
||||
MCPCacheRepository.clearCache();
|
||||
console.log("✅ Cache antigo limpo");
|
||||
|
||||
// 3. Forçar sincronização completa
|
||||
console.log("📥 Iniciando download completo de servidores PulseMCP...");
|
||||
|
||||
const result = await MCPSyncService.syncAll((progress) => {
|
||||
const percentage =
|
||||
progress.total > 0
|
||||
? Math.round((progress.current / progress.total) * 100)
|
||||
: 0;
|
||||
console.log(
|
||||
`📊 Progresso: ${progress.current}/${progress.total} (${percentage}%) - Página ${progress.page}/${progress.totalPages}`
|
||||
);
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ Sincronização concluída com sucesso!`);
|
||||
console.log(`📊 Total de servidores baixados: ${result.totalDownloaded}`);
|
||||
console.log(`⏱️ Tempo total: ${Math.round(result.duration / 1000)}s`);
|
||||
|
||||
// 4. Verificar estatísticas do cache
|
||||
const stats = MCPCacheRepository.getStats();
|
||||
console.log(`📈 Estatísticas do cache:`);
|
||||
console.log(` - Total de itens: ${stats.totalItems}`);
|
||||
console.log(
|
||||
` - Última sincronização: ${
|
||||
stats.lastSync ? new Date(stats.lastSync).toLocaleString() : "Nunca"
|
||||
}`
|
||||
);
|
||||
console.log(` - Cache válido: ${stats.isValid ? "Sim" : "Não"}`);
|
||||
|
||||
// 5. Testar busca de alguns servidores
|
||||
console.log("🔍 Testando busca de servidores...");
|
||||
const sampleServers = MCPCacheRepository.search({ limit: 5 });
|
||||
console.log(
|
||||
`📋 Encontrados ${sampleServers.length} servidores no cache:`
|
||||
);
|
||||
|
||||
sampleServers.forEach((server, index) => {
|
||||
console.log(
|
||||
` ${index + 1}. ${server.content_name} (${server.category})`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
console.error("❌ Sincronização falhou:");
|
||||
result.errors.forEach((error) => console.error(` - ${error}`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Erro durante sincronização:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Executar se chamado diretamente
|
||||
if (require.main === module) {
|
||||
forcePulseMCPSync()
|
||||
.then(() => {
|
||||
console.log("🎉 Script concluído");
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("💥 Erro fatal:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
export { forcePulseMCPSync };
|
||||
+1
-66
@@ -92,56 +92,7 @@ function runMigrations(db: Database.Database): void {
|
||||
console.log("✅ Migração 002_mcp_installations aplicada");
|
||||
}
|
||||
|
||||
// Aplicar migração para tabela MCP marketplace cache se não foi aplicada
|
||||
if (!appliedNames.has("003_mcp_marketplace_cache")) {
|
||||
console.log("📋 Aplicando migração: 003_mcp_marketplace_cache");
|
||||
|
||||
db.exec(`
|
||||
-- Tabela principal de cache
|
||||
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
|
||||
CREATE TABLE IF NOT EXISTS mcp_cache_metadata (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Índices
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_cache_category ON mcp_marketplace_cache(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_cache_subfield ON mcp_marketplace_cache(subfield);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_cache_rating ON mcp_marketplace_cache(rating DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_cache_owner_repo ON mcp_marketplace_cache(owner, repo);
|
||||
CREATE INDEX IF NOT EXISTS idx_mcp_cache_search ON mcp_marketplace_cache(content_name, description);
|
||||
`);
|
||||
|
||||
// Registrar migração
|
||||
db.prepare("INSERT INTO _migrations (name, applied_at) VALUES (?, ?)").run(
|
||||
"003_mcp_marketplace_cache",
|
||||
Date.now()
|
||||
);
|
||||
|
||||
console.log("✅ Migração 003_mcp_marketplace_cache aplicada");
|
||||
}
|
||||
// Migração de marketplace removida
|
||||
|
||||
// Inicializar extensão vetorial se disponível
|
||||
try {
|
||||
@@ -231,22 +182,6 @@ if (require.main === module) {
|
||||
])
|
||||
.then(async () => {
|
||||
console.log("🎉 Inicialização concluída");
|
||||
|
||||
// Iniciar sincronização do marketplace se necessário
|
||||
try {
|
||||
const { MCPSyncService } = await import("@/lib/services/mcp-sync");
|
||||
const { MCPCacheRepository } = await import("@/database/repositories/mcp-cache");
|
||||
|
||||
if (MCPCacheRepository.needsSync()) {
|
||||
console.log("🔄 Iniciando sincronização inicial do marketplace...");
|
||||
MCPSyncService.backgroundSync();
|
||||
} else {
|
||||
console.log("✅ Cache do marketplace já está atualizado");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("⚠️ Não foi possível inicializar sincronização do marketplace:", error);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
async function testPulseMCPDirect() {
|
||||
console.log("🔍 Testando PulseMCP API diretamente...");
|
||||
|
||||
try {
|
||||
const url =
|
||||
"https://api.pulsemcp.com/v0beta/servers?count_per_page=5&offset=0";
|
||||
|
||||
console.log(`📡 Fazendo requisição para: ${url}`);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0 (https://ollahub.com)",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`📊 Status: ${response.status} ${response.statusText}`);
|
||||
console.log(`📋 Headers:`, Object.fromEntries(response.headers.entries()));
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`❌ Erro ${response.status}:`, errorText);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`✅ Sucesso! Total de servidores: ${data.total_count}`);
|
||||
console.log(`📋 Primeiros 3 servidores:`);
|
||||
|
||||
data.servers.slice(0, 3).forEach((server: any, index: number) => {
|
||||
console.log(` ${index + 1}. ${server.name}`);
|
||||
console.log(` - Package: ${server.package_name || "N/A"}`);
|
||||
console.log(` - Registry: ${server.package_registry || "N/A"}`);
|
||||
console.log(` - Stars: ${server.github_stars || 0}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ Erro na requisição:", error);
|
||||
}
|
||||
}
|
||||
|
||||
testPulseMCPDirect()
|
||||
.then(() => {
|
||||
console.log("🎉 Teste concluído");
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("💥 Erro fatal:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
import { MCPCacheRepository } from "../database/repositories/mcp-cache";
|
||||
import { MCPSyncService } from "../lib/services/mcp-sync";
|
||||
import { PulseMCPService } from "../lib/services/pulsemcp";
|
||||
|
||||
async function testSimpleSync() {
|
||||
console.log("🔄 Testando sincronização simples com PulseMCP...");
|
||||
|
||||
try {
|
||||
// 1. Verificar se PulseMCP API está funcionando
|
||||
console.log("🔍 Verificando conectividade com PulseMCP API...");
|
||||
const isHealthy = await PulseMCPService.checkHealth();
|
||||
|
||||
if (!isHealthy) {
|
||||
console.error("❌ PulseMCP API não está acessível");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("✅ PulseMCP API está funcionando");
|
||||
|
||||
// 2. Limpar cache antigo
|
||||
console.log("🗑️ Limpando cache antigo...");
|
||||
MCPCacheRepository.clearCache();
|
||||
console.log("✅ Cache antigo limpo");
|
||||
|
||||
// 3. Testar chamada direta à API
|
||||
console.log("📡 Testando chamada direta à API PulseMCP...");
|
||||
const response = await PulseMCPService.getAllServers();
|
||||
console.log(
|
||||
`✅ API retornou ${response.servers.length} servidores (total: ${response.total_count})`
|
||||
);
|
||||
|
||||
// 4. Salvar alguns servidores no banco para teste
|
||||
console.log("💾 Salvando primeiros 10 servidores no banco...");
|
||||
const testBatch = response.servers.slice(0, 10);
|
||||
MCPCacheRepository.saveBatch(testBatch);
|
||||
console.log("✅ Servidores salvos no banco");
|
||||
|
||||
// 5. Verificar se foram salvos corretamente
|
||||
console.log("🔍 Verificando dados salvos...");
|
||||
const savedServers = MCPCacheRepository.search({ limit: 10 });
|
||||
console.log(`📋 Encontrados ${savedServers.length} servidores no banco:`);
|
||||
|
||||
savedServers.forEach((server, index) => {
|
||||
console.log(
|
||||
` ${index + 1}. ${server.content_name} (${server.category})`
|
||||
);
|
||||
});
|
||||
|
||||
// 6. Testar sincronização completa
|
||||
console.log("🚀 Iniciando sincronização completa...");
|
||||
const result = await MCPSyncService.syncAll((progress) => {
|
||||
const percentage =
|
||||
progress.total > 0
|
||||
? Math.round((progress.current / progress.total) * 100)
|
||||
: 0;
|
||||
console.log(
|
||||
`📊 Progresso: ${progress.current}/${progress.total} (${percentage}%)`
|
||||
);
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ Sincronização concluída com sucesso!`);
|
||||
console.log(`📊 Total de servidores baixados: ${result.totalDownloaded}`);
|
||||
console.log(`⏱️ Tempo total: ${Math.round(result.duration / 1000)}s`);
|
||||
|
||||
// 7. Verificar estatísticas finais
|
||||
const stats = MCPCacheRepository.getStats();
|
||||
console.log(`📈 Estatísticas finais do cache:`);
|
||||
console.log(` - Total de itens: ${stats.totalItems}`);
|
||||
console.log(
|
||||
` - Última sincronização: ${
|
||||
stats.lastSync ? new Date(stats.lastSync).toLocaleString() : "Nunca"
|
||||
}`
|
||||
);
|
||||
console.log(` - Cache válido: ${stats.isValid ? "Sim" : "Não"}`);
|
||||
} else {
|
||||
console.error("❌ Sincronização falhou:");
|
||||
result.errors.forEach((error) => console.error(` - ${error}`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Erro durante teste:", error);
|
||||
}
|
||||
}
|
||||
|
||||
testSimpleSync();
|
||||
|
||||
export { testSimpleSync };
|
||||
Reference in new issue
Block a user