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:
Evandro Fonseca Junior committed 2025-10-29 15:35:15 -03:00
1 parent 4739a13458
commit f96d4ed696
34 files changed
+260 -3775

No files matched your search

+1 -1
View File
@@ -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);
-68
View File
@@ -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 }
);
}
}
-13
View File
@@ -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}`);
}
+15 -43
View File
@@ -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`);
-201
View File
@@ -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,
});
}
}
+2 -61
View File
@@ -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);
-91
View File
@@ -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 }
);
}
}
-69
View File
@@ -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 }
);
}
}
+83 -7
View File
@@ -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"
);
}
}
+7 -7
View File
@@ -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>
);
-136
View File
@@ -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>
);
}
+4 -4
View File
@@ -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
-74
View File
@@ -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>
);
}
-31
View File
@@ -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
View File
@@ -1 +0,0 @@
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-544
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+49 -2
View File
@@ -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 };
}
+63 -20
View File
@@ -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") ||
-278
View File
@@ -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,
};
}
}
}
-462
View File
@@ -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
View File
@@ -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
-66
View File
@@ -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();
-187
View File
@@ -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!");
-90
View File
@@ -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
View File
@@ -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) => {
-53
View File
@@ -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);
});
-87
View File
@@ -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 };