feat: add MCP marketplace and custom scrollbar styles
Add comprehensive CSS styles for the MCP Marketplace interface, including card hover effects, responsive grid layout, modal backdrop blur, category badges, rating stars, install buttons, and loading animations. Also include custom scrollbar styling for the model selector to enhance user experience and visual consistency.
This commit is contained in:
1 parent
d5dd3a9f09
commit
ce010d39ee
31 files changed
+4127
-45
No files matched your search
@@ -0,0 +1,68 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPInstallationStatus } from "@/lib/types/mcp";
|
||||
import { DeepNLPService } from "@/lib/services/deepnlp";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { mcpId } = await request.json();
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Installing MCP: ${mcpId}`);
|
||||
|
||||
// Verificar se já está instalado
|
||||
if (MCPRepository.isInstalled(mcpId)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `MCP ${mcpId} is already installed`,
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Obter configuração completa do servidor
|
||||
console.log(`Fetching server config for ${mcpId}`);
|
||||
const serverConfig = await DeepNLPService.getServerConfig(mcpId);
|
||||
|
||||
if (!serverConfig.items || serverConfig.items.length === 0) {
|
||||
throw new Error(`No configuration found for MCP ${mcpId}`);
|
||||
}
|
||||
|
||||
// Obter ferramentas disponíveis
|
||||
console.log(`Fetching server tools for ${mcpId}`);
|
||||
const tools = await DeepNLPService.getServerTools(mcpId);
|
||||
|
||||
// Salvar configuração localmente
|
||||
console.log(`Saving MCP ${mcpId} to local database`);
|
||||
MCPRepository.saveInstallation(mcpId, serverConfig.items[0], tools);
|
||||
|
||||
const result: MCPInstallationStatus = {
|
||||
success: true,
|
||||
message: `MCP ${mcpId} installed successfully`,
|
||||
mcpId,
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
installation: result,
|
||||
config: serverConfig.items[0],
|
||||
tools: tools,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error installing MCP:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.message || "Failed to install MCP" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
MCPProvider,
|
||||
MCPSearchParams,
|
||||
DeepNLPSearchParams,
|
||||
} from "@/lib/types/mcp";
|
||||
import { DeepNLPService } from "@/lib/services/deepnlp";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
|
||||
import { MCPSyncService } from "@/lib/services/mcp-sync";
|
||||
|
||||
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`);
|
||||
|
||||
// Transformar para formato interno
|
||||
const mcps = cachedMcps.map((item) =>
|
||||
DeepNLPService.transformToMCPProvider(item)
|
||||
);
|
||||
|
||||
// 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 se cache vazio
|
||||
console.log("Cache empty, falling back to DeepNLP API");
|
||||
|
||||
const deepnlpCategory =
|
||||
category && category !== "all"
|
||||
? DeepNLPService.getCategoryMapping(category)
|
||||
: undefined;
|
||||
|
||||
const deepnlpParams: DeepNLPSearchParams = {
|
||||
query: search,
|
||||
category: deepnlpCategory,
|
||||
page_id: page,
|
||||
count_per_page: limit,
|
||||
mode: "list",
|
||||
};
|
||||
|
||||
console.log("Calling DeepNLP API with params:", deepnlpParams);
|
||||
|
||||
const response = await DeepNLPService.searchMCPs(deepnlpParams);
|
||||
const mcps = response.items.map((item) =>
|
||||
DeepNLPService.transformToMCPProvider(item)
|
||||
);
|
||||
|
||||
// Adicionar status de instalação
|
||||
const installedMCPs = MCPRepository.listInstalled();
|
||||
const mcpsWithStatus = mcps.map((mcp) => ({
|
||||
...mcp,
|
||||
installed: installedMCPs.includes(mcp.id),
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcps: mcpsWithStatus,
|
||||
total: response.total_hits,
|
||||
hasMore: response.total_hits > (page + 1) * limit,
|
||||
cached: false,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching MCPs from DeepNLP:", 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,
|
||||
downloads: 20,
|
||||
capabilities: ["geocoding", "directions", "places"],
|
||||
repository:
|
||||
"https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps",
|
||||
installed: false,
|
||||
subfield: "MAP",
|
||||
field: "MCP SERVER",
|
||||
},
|
||||
];
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcps: mockMCPs,
|
||||
total: mockMCPs.length,
|
||||
hasMore: false,
|
||||
fallback: true,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { MCPInstallationStatus } from "@/lib/types/mcp";
|
||||
import { MCPRepository } from "@/database/repositories/mcp";
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
try {
|
||||
const { mcpId } = await request.json();
|
||||
|
||||
if (!mcpId) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: "MCP ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Uninstalling MCP: ${mcpId}`);
|
||||
|
||||
// Verificar se está instalado
|
||||
if (!MCPRepository.isInstalled(mcpId)) {
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: `MCP ${mcpId} is not installed`,
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// Remover do banco local
|
||||
MCPRepository.removeInstallation(mcpId);
|
||||
|
||||
const result: MCPInstallationStatus = {
|
||||
success: true,
|
||||
message: `MCP ${mcpId} uninstalled successfully`,
|
||||
mcpId,
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
installation: result,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error uninstalling MCP:", error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error.message || "Failed to uninstall MCP" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { deleteModel } from "@/lib/ollama";
|
||||
|
||||
export async function DELETE(request: NextRequest) {
|
||||
try {
|
||||
const { modelName } = await request.json();
|
||||
|
||||
if (!modelName) {
|
||||
return NextResponse.json(
|
||||
{ error: "Model name is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
await deleteModel(modelName);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Model ${modelName} deleted successfully`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting model:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to delete model" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { searchRemoteModels } from "@/lib/ollama";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const query = searchParams.get("q") || undefined;
|
||||
|
||||
const remoteModels = await searchRemoteModels(query);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
models: remoteModels,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error searching remote models:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to search remote models" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user