Files
OllaHub/app/api/mcp/list/route.ts
T
Evandro Fonseca Junior f19bf10ef3 feat(mcp): enhance MCP installation with status tracking and custom configs
- Add /mcp-servers/ to .gitignore for MCP server installations
- Extend mcp_installations table with status, logs, and validation fields
- Refactor MCP install route to support custom configs, enable logs, and async installation
- Improve MCPSyncService with promise-based syncing to prevent concurrent operations

This update improves MCP management by adding robust installation tracking, custom configuration support, and better error handling for a more reliable user experience.
2025-10-28 21:59:00 -03:00

202 lines
6.1 KiB
TypeScript

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