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"
);
}
}