refactor(api): migrate MCP listing from DeepNLP to PulseMCP service
- Updated imports to use PulseMCPSearchParams and PulseMCPService instead of DeepNLP equivalents - Replaced DeepNLP API calls with PulseMCP API endpoints and parameters - Modified cached MCP transformation to manual mapping for PulseMCP data format - Enhanced error handling and retry logic in server tools retrieval - Added PulseMCP API base URL constant for centralized configuration This change switches the underlying MCP provider from DeepNLP to PulseMCP, ensuring compatibility with the new API while maintaining existing functionality for MCP listing and tool retrieval.
This commit is contained in:
1 parent
ce010d39ee
commit
1042f2136b
15 files changed
+1237
-398
No files matched your search
+58
-27
@@ -2,13 +2,15 @@ import { NextResponse } from "next/server";
|
||||
import {
|
||||
MCPProvider,
|
||||
MCPSearchParams,
|
||||
DeepNLPSearchParams,
|
||||
PulseMCPSearchParams,
|
||||
} from "@/lib/types/mcp";
|
||||
import { DeepNLPService } from "@/lib/services/deepnlp";
|
||||
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) {
|
||||
@@ -55,10 +57,25 @@ export async function GET(request: Request) {
|
||||
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)
|
||||
);
|
||||
// Os dados já estão no formato interno após transformação no saveBatch
|
||||
const mcps = cachedMcps.map((item) => ({
|
||||
id: item.id,
|
||||
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();
|
||||
@@ -81,32 +98,46 @@ export async function GET(request: Request) {
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Fallback: buscar da API se cache vazio
|
||||
console.log("Cache empty, falling back to DeepNLP API");
|
||||
// 3. Fallback: buscar da API PulseMCP se cache vazio
|
||||
console.log("Cache empty, falling back to PulseMCP API");
|
||||
|
||||
const deepnlpCategory =
|
||||
category && category !== "all"
|
||||
? DeepNLPService.getCategoryMapping(category)
|
||||
: undefined;
|
||||
|
||||
const deepnlpParams: DeepNLPSearchParams = {
|
||||
const pulseMCPParams: PulseMCPSearchParams = {
|
||||
query: search,
|
||||
category: deepnlpCategory,
|
||||
page_id: page,
|
||||
count_per_page: limit,
|
||||
mode: "list",
|
||||
offset: page * limit,
|
||||
};
|
||||
|
||||
console.log("Calling DeepNLP API with params:", deepnlpParams);
|
||||
console.log("Calling PulseMCP API with params:", pulseMCPParams);
|
||||
|
||||
const response = await DeepNLPService.searchMCPs(deepnlpParams);
|
||||
const mcps = response.items.map((item) =>
|
||||
DeepNLPService.transformToMCPProvider(item)
|
||||
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) => ({
|
||||
const mcpsWithStatus = mcps.map((mcp: MCPProvider) => ({
|
||||
...mcp,
|
||||
installed: installedMCPs.includes(mcp.id),
|
||||
}));
|
||||
@@ -114,12 +145,12 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
mcps: mcpsWithStatus,
|
||||
total: response.total_hits,
|
||||
hasMore: response.total_hits > (page + 1) * limit,
|
||||
total: data.total_count,
|
||||
hasMore: data.total_count > (page + 1) * limit,
|
||||
cached: false,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching MCPs from DeepNLP:", error);
|
||||
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");
|
||||
@@ -136,13 +167,13 @@ export async function GET(request: Request) {
|
||||
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",
|
||||
config: [],
|
||||
tools: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
+47
-36
@@ -1,6 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Package, Star, Download, Eye, ExternalLink, CheckCircle, Zap } from "lucide-react";
|
||||
import {
|
||||
Package,
|
||||
Star,
|
||||
Eye,
|
||||
ExternalLink,
|
||||
CheckCircle,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { MCPProvider } from "@/lib/types/mcp";
|
||||
|
||||
interface MCPCardProps {
|
||||
@@ -10,19 +17,34 @@ interface MCPCardProps {
|
||||
onViewDetails: () => void;
|
||||
}
|
||||
|
||||
export function MCPCard({ mcp, onInstall, onUninstall, onViewDetails }: MCPCardProps) {
|
||||
export function MCPCard({
|
||||
mcp,
|
||||
onInstall,
|
||||
onUninstall,
|
||||
onViewDetails,
|
||||
}: MCPCardProps) {
|
||||
const getCategoryColor = (category: string) => {
|
||||
switch (category) {
|
||||
case "map": return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200";
|
||||
case "browser": return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200";
|
||||
case "office": return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200";
|
||||
case "search": return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200";
|
||||
case "database": return "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200";
|
||||
case "finance": return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200";
|
||||
case "code": return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
|
||||
case "chart": return "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200";
|
||||
case "payment": return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200";
|
||||
default: return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200";
|
||||
case "map":
|
||||
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200";
|
||||
case "browser":
|
||||
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200";
|
||||
case "office":
|
||||
return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200";
|
||||
case "search":
|
||||
return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200";
|
||||
case "database":
|
||||
return "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200";
|
||||
case "finance":
|
||||
return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200";
|
||||
case "code":
|
||||
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
|
||||
case "chart":
|
||||
return "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200";
|
||||
case "payment":
|
||||
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,39 +54,32 @@ export function MCPCard({ mcp, onInstall, onUninstall, onViewDetails }: MCPCardP
|
||||
return downloads.toString();
|
||||
};
|
||||
|
||||
const isOfficial = mcp.tags?.includes('official') || false;
|
||||
const toolCount = mcp.tools?.length || mcp.capabilities?.length || 0;
|
||||
const isOfficial = mcp.tags?.includes("official") || false;
|
||||
const toolCount = mcp.tools?.length || 0;
|
||||
|
||||
return (
|
||||
<div className="bg-[var(--surface)] border border-[var(--border)] rounded-lg p-4 hover:shadow-md transition-all duration-200 hover:border-[var(--primary)]/50 mcp-card">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{mcp.iconUrl ? (
|
||||
<img
|
||||
src={mcp.iconUrl}
|
||||
alt={mcp.name}
|
||||
className="h-8 w-8 rounded-md object-cover"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
e.currentTarget.nextElementSibling?.classList.remove('hidden');
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div className={`p-2 bg-[var(--primary)]/10 rounded-md ${mcp.iconUrl ? 'hidden' : ''}`}>
|
||||
<div className="p-2 bg-[var(--primary)]/10 rounded-md">
|
||||
<Package className="h-4 w-4 text-[var(--primary)]" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-sm">{mcp.name}</h3>
|
||||
{isOfficial && (
|
||||
<CheckCircle className="h-3 w-3 text-green-500" />
|
||||
)}
|
||||
{isOfficial && <CheckCircle className="h-3 w-3 text-green-500" />}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">by {mcp.author}</p>
|
||||
<p className="text-xs text-[var(--muted-foreground)]">
|
||||
by {mcp.author}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium mcp-category-badge ${getCategoryColor(mcp.category)}`}>
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs font-medium mcp-category-badge ${getCategoryColor(
|
||||
mcp.category
|
||||
)}`}
|
||||
>
|
||||
{mcp.category}
|
||||
</span>
|
||||
</div>
|
||||
@@ -81,10 +96,6 @@ export function MCPCard({ mcp, onInstall, onUninstall, onViewDetails }: MCPCardP
|
||||
<span>{mcp.rating}</span>
|
||||
<span>({mcp.totalRatings})</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="h-3 w-3" />
|
||||
<span>{formatDownloads(mcp.downloads)}</span>
|
||||
</div>
|
||||
{toolCount > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Zap className="h-3 w-3" />
|
||||
@@ -122,7 +133,7 @@ export function MCPCard({ mcp, onInstall, onUninstall, onViewDetails }: MCPCardP
|
||||
<Eye className="h-4 w-4" />
|
||||
Details
|
||||
</button>
|
||||
|
||||
|
||||
{mcp.installed ? (
|
||||
<button
|
||||
onClick={onUninstall}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { X, Package, Star, Download, ExternalLink, CheckCircle } from "lucide-react";
|
||||
import { X, Package, Star, ExternalLink, CheckCircle } from "lucide-react";
|
||||
import { MCPProvider } from "@/lib/types/mcp";
|
||||
import { MCPRatingStars } from "./MCPRatingStars";
|
||||
|
||||
@@ -12,36 +12,47 @@ interface MCPDetailModalProps {
|
||||
onInstall: () => void;
|
||||
}
|
||||
|
||||
export function MCPDetailModal({ mcp, isOpen, onClose, onInstall }: MCPDetailModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<"about" | "capabilities" | "screenshots" | "configuration">("about");
|
||||
export function MCPDetailModal({
|
||||
mcp,
|
||||
isOpen,
|
||||
onClose,
|
||||
onInstall,
|
||||
}: MCPDetailModalProps) {
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
"about" | "capabilities" | "screenshots" | "configuration"
|
||||
>("about");
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const formatDownloads = (downloads: number) => {
|
||||
if (downloads >= 1000000) return `${(downloads / 1000000).toFixed(1)}M`;
|
||||
if (downloads >= 1000) return `${(downloads / 1000).toFixed(1)}K`;
|
||||
return downloads.toString();
|
||||
};
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
switch (category) {
|
||||
case "map": return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200";
|
||||
case "browser": return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200";
|
||||
case "office": return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200";
|
||||
case "search": return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200";
|
||||
case "database": return "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200";
|
||||
case "finance": return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200";
|
||||
case "code": return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
|
||||
case "chart": return "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200";
|
||||
case "payment": return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200";
|
||||
default: return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200";
|
||||
case "map":
|
||||
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200";
|
||||
case "browser":
|
||||
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200";
|
||||
case "office":
|
||||
return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200";
|
||||
case "search":
|
||||
return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200";
|
||||
case "database":
|
||||
return "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200";
|
||||
case "finance":
|
||||
return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200";
|
||||
case "code":
|
||||
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
|
||||
case "chart":
|
||||
return "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200";
|
||||
case "payment":
|
||||
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
||||
|
||||
|
||||
<div className="relative w-full max-w-4xl h-[90vh] bg-[var(--background)] border border-[var(--border)] rounded-lg shadow-xl mcp-detail-modal">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-[var(--border)]">
|
||||
@@ -53,7 +64,11 @@ export function MCPDetailModal({ mcp, isOpen, onClose, onInstall }: MCPDetailMod
|
||||
<h2 className="text-2xl font-bold">{mcp.name}</h2>
|
||||
<p className="text-[var(--muted-foreground)]">by {mcp.author}</p>
|
||||
</div>
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium mcp-category-badge ${getCategoryColor(mcp.category)}`}>
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-sm font-medium mcp-category-badge ${getCategoryColor(
|
||||
mcp.category
|
||||
)}`}
|
||||
>
|
||||
{mcp.category}
|
||||
</span>
|
||||
{mcp.installed && (
|
||||
@@ -80,11 +95,9 @@ export function MCPDetailModal({ mcp, isOpen, onClose, onInstall }: MCPDetailMod
|
||||
({mcp.totalRatings} reviews)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Download className="h-4 w-4 text-[var(--muted-foreground)]" />
|
||||
<span className="text-sm">{formatDownloads(mcp.downloads)} downloads</span>
|
||||
</div>
|
||||
<span className="text-sm text-[var(--muted-foreground)]">Version {mcp.version}</span>
|
||||
<span className="text-sm text-[var(--muted-foreground)]">
|
||||
Version {mcp.version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -171,14 +184,16 @@ export function MCPDetailModal({ mcp, isOpen, onClose, onInstall }: MCPDetailMod
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-3">Capabilities</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{(mcp.tools || mcp.capabilities || []).map((capability, index) => (
|
||||
{(mcp.tools || []).map((capability, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2 p-3 bg-[var(--surface)] rounded-md border border-[var(--border)]"
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
||||
<span className="text-sm">
|
||||
{typeof capability === 'string' ? capability : (capability as any).name || String(capability)}
|
||||
{typeof capability === "string"
|
||||
? capability
|
||||
: (capability as any).name || String(capability)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -189,35 +204,54 @@ export function MCPDetailModal({ mcp, isOpen, onClose, onInstall }: MCPDetailMod
|
||||
{activeTab === "configuration" && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-3">Installation Configuration</h3>
|
||||
<h3 className="text-lg font-semibold mb-3">
|
||||
Installation Configuration
|
||||
</h3>
|
||||
{mcp.config && mcp.config.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{mcp.config.map((configItem, index) => (
|
||||
<div key={index} className="bg-[var(--surface)] border border-[var(--border)] rounded-lg p-4">
|
||||
<h4 className="font-medium mb-3">Configuration {index + 1}</h4>
|
||||
<div
|
||||
key={index}
|
||||
className="bg-[var(--surface)] border border-[var(--border)] rounded-lg p-4"
|
||||
>
|
||||
<h4 className="font-medium mb-3">
|
||||
Configuration {index + 1}
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{configItem.mcpServers && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-[var(--muted-foreground)] mb-2">Command</h5>
|
||||
<h5 className="text-sm font-medium text-[var(--muted-foreground)] mb-2">
|
||||
Command
|
||||
</h5>
|
||||
<code className="block bg-[var(--background)] p-2 rounded text-sm">
|
||||
{configItem.mcpServers.command} {configItem.mcpServers.arguments?.join(' ')}
|
||||
{configItem.mcpServers.command}{" "}
|
||||
{configItem.mcpServers.arguments?.join(" ")}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
{configItem.mcpServers?.env && Object.keys(configItem.mcpServers.env).length > 0 && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-[var(--muted-foreground)] mb-2">Environment Variables</h5>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(configItem.mcpServers.env).map(([key, value]) => (
|
||||
<div key={key} className="flex items-center gap-2">
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-sm font-mono">
|
||||
{key}={String(value)}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
{configItem.mcpServers?.env &&
|
||||
Object.keys(configItem.mcpServers.env).length >
|
||||
0 && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-[var(--muted-foreground)] mb-2">
|
||||
Environment Variables
|
||||
</h5>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(
|
||||
configItem.mcpServers.env
|
||||
).map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-sm font-mono">
|
||||
{key}={String(value)}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -232,25 +266,45 @@ export function MCPDetailModal({ mcp, isOpen, onClose, onInstall }: MCPDetailMod
|
||||
|
||||
{mcp.tools && mcp.tools.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-3">Available Tools</h3>
|
||||
<h3 className="text-lg font-semibold mb-3">
|
||||
Available Tools
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{mcp.tools.map((tool, index) => (
|
||||
<div key={index} className="bg-[var(--surface)] border border-[var(--border)] rounded-lg p-4">
|
||||
<div
|
||||
key={index}
|
||||
className="bg-[var(--surface)] border border-[var(--border)] rounded-lg p-4"
|
||||
>
|
||||
<h4 className="font-medium mb-2">{tool.name}</h4>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">{tool.description}</p>
|
||||
<p className="text-sm text-[var(--muted-foreground)] mb-3">
|
||||
{tool.description}
|
||||
</p>
|
||||
{tool.input_schema && (
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-[var(--muted-foreground)] mb-2">Parameters</h5>
|
||||
<h5 className="text-sm font-medium text-[var(--muted-foreground)] mb-2">
|
||||
Parameters
|
||||
</h5>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(tool.input_schema.properties || {}).map(([param, schema]) => (
|
||||
<div key={param} className="flex items-center gap-2">
|
||||
{Object.entries(
|
||||
tool.input_schema.properties || {}
|
||||
).map(([param, schema]) => (
|
||||
<div
|
||||
key={param}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<code className="bg-[var(--background)] px-2 py-1 rounded text-sm font-mono">
|
||||
{param}
|
||||
</code>
|
||||
<span className="text-xs text-[var(--muted-foreground)]">
|
||||
{typeof schema === 'object' && schema ? (schema as any).type || 'any' : 'any'}
|
||||
{tool.input_schema.required?.includes(param) && (
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
{typeof schema === "object" && schema
|
||||
? (schema as any).type || "any"
|
||||
: "any"}
|
||||
{tool.input_schema.required?.includes(
|
||||
param
|
||||
) && (
|
||||
<span className="text-red-500 ml-1">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -269,23 +323,10 @@ export function MCPDetailModal({ mcp, isOpen, onClose, onInstall }: MCPDetailMod
|
||||
{activeTab === "screenshots" && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-3">Screenshots</h3>
|
||||
{mcp.screenshotUrls && mcp.screenshotUrls.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{mcp.screenshotUrls.map((url, index) => (
|
||||
<img
|
||||
key={index}
|
||||
src={url}
|
||||
alt={`Screenshot ${index + 1}`}
|
||||
className="w-full h-48 object-cover rounded-md border border-[var(--border)]"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-[var(--muted-foreground)]">
|
||||
<Package className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No screenshots available</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-center py-12 text-[var(--muted-foreground)]">
|
||||
<Package className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No screenshots available</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { X, Search, Filter, Package, Star, Download, Eye } from "lucide-react";
|
||||
import { X, Search, Filter, Package, Star, Eye } from "lucide-react";
|
||||
import { MCPProvider } from "@/lib/types/mcp";
|
||||
import { MCPCard } from "./MCPCard";
|
||||
import { MCPCategoryFilter } from "./MCPCategoryFilter";
|
||||
@@ -22,7 +22,7 @@ export function MCPMarketplaceModal({
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||
const [sortBy, setSortBy] = useState<
|
||||
"rating" | "downloads" | "name" | "recent" | "total_ratings" | "updated_at"
|
||||
"rating" | "name" | "recent" | "total_ratings" | "updated_at"
|
||||
>("rating");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||
const [selectedMCP, setSelectedMCP] = useState<MCPProvider | null>(null);
|
||||
@@ -283,7 +283,6 @@ export function MCPMarketplaceModal({
|
||||
className="px-3 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-md text-sm"
|
||||
>
|
||||
<option value="rating">Por Avaliação</option>
|
||||
<option value="downloads">Por Downloads</option>
|
||||
<option value="name">Por Nome</option>
|
||||
<option value="recent">Mais Recentes</option>
|
||||
<option value="total_ratings">Por Nº de Reviews</option>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,4 +1,5 @@
|
||||
import { getDatabase } from "@/database/database";
|
||||
import { PulseMCPService } from "@/lib/services/pulsemcp";
|
||||
|
||||
const db = getDatabase();
|
||||
|
||||
@@ -99,25 +100,42 @@ export class MCPCacheRepository {
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (const mcp of mcps) {
|
||||
const [owner, repo] = (mcp.id || "").split("/");
|
||||
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(
|
||||
mcp.id || "",
|
||||
owner || "",
|
||||
repo || "",
|
||||
mcp.content_name || "",
|
||||
mcp.publisher_id || "",
|
||||
mcp.description || null,
|
||||
mcp.category || null,
|
||||
mcp.subfield || null,
|
||||
mcp.field || null,
|
||||
parseFloat(mcp.rating || "0"),
|
||||
parseInt(mcp.review_cnt || "0"),
|
||||
mcp.content_tag_list || null,
|
||||
mcp.thumbnail_picture || null,
|
||||
mcp.website || null,
|
||||
mcp.detail_url || null,
|
||||
mcp.ext_info ? JSON.stringify(mcp.ext_info) : null,
|
||||
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
|
||||
);
|
||||
|
||||
+161
-108
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
DeepNLPSearchParams,
|
||||
DeepNLPResponse,
|
||||
MCPServerConfig,
|
||||
MCPTool,
|
||||
MCPProvider
|
||||
import {
|
||||
DeepNLPSearchParams,
|
||||
DeepNLPResponse,
|
||||
MCPServerConfig,
|
||||
MCPTool,
|
||||
MCPProvider,
|
||||
} from "@/lib/types/mcp";
|
||||
import { MCPCache } from "@/lib/cache/mcp-cache";
|
||||
|
||||
@@ -12,30 +12,33 @@ const DEEPNLP_API_BASE = "https://www.deepnlp.org/api/mcp_marketplace/v1";
|
||||
export class DeepNLPService {
|
||||
// Mapear categorias do projeto para categorias DeepNLP
|
||||
private static categoryMap = {
|
||||
productivity: ['office'],
|
||||
development: ['code', 'browser'],
|
||||
data: ['database'],
|
||||
integration: ['search', 'payment'],
|
||||
other: ['map', 'chart', 'finance']
|
||||
productivity: ["office"],
|
||||
development: ["code", "browser"],
|
||||
data: ["database"],
|
||||
integration: ["search", "payment"],
|
||||
other: ["map", "chart", "finance"],
|
||||
};
|
||||
|
||||
// Mapear categorias DeepNLP para categorias internas
|
||||
private static reverseCategoryMap: Record<string, MCPProvider['category']> = {
|
||||
'office': 'office',
|
||||
'code': 'code',
|
||||
'browser': 'browser',
|
||||
'database': 'database',
|
||||
'search': 'search',
|
||||
'payment': 'payment',
|
||||
'map': 'map',
|
||||
'chart': 'chart',
|
||||
'finance': 'finance'
|
||||
private static reverseCategoryMap: Record<string, MCPProvider["category"]> = {
|
||||
office: "office",
|
||||
code: "code",
|
||||
browser: "browser",
|
||||
database: "database",
|
||||
search: "search",
|
||||
payment: "payment",
|
||||
map: "map",
|
||||
chart: "chart",
|
||||
finance: "finance",
|
||||
};
|
||||
|
||||
/**
|
||||
* Buscar servidores MCP na API DeepNLP
|
||||
*/
|
||||
static async searchMCPs(params: DeepNLPSearchParams, retryCount = 0): Promise<DeepNLPResponse> {
|
||||
static async searchMCPs(
|
||||
params: DeepNLPSearchParams,
|
||||
retryCount = 0
|
||||
): Promise<DeepNLPResponse> {
|
||||
const maxRetries = 3;
|
||||
const baseDelay = 1000; // 1 segundo
|
||||
|
||||
@@ -43,67 +46,88 @@ export class DeepNLPService {
|
||||
// Verificar cache primeiro
|
||||
const cacheKey = MCPCache.generateKey(params);
|
||||
const cachedResult = MCPCache.get(cacheKey);
|
||||
|
||||
|
||||
if (cachedResult) {
|
||||
console.log('Returning cached search results');
|
||||
console.log("Returning cached search results");
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const url = new URL(DEEPNLP_API_BASE);
|
||||
|
||||
// Adicionar parâmetros de busca
|
||||
if (params.query) url.searchParams.set('query', params.query);
|
||||
if (params.category) url.searchParams.set('category', params.category);
|
||||
if (params.id) url.searchParams.set('id', params.id);
|
||||
if (params.page_id !== undefined) url.searchParams.set('page_id', params.page_id.toString());
|
||||
if (params.count_per_page !== undefined) url.searchParams.set('count_per_page', params.count_per_page.toString());
|
||||
if (params.offset !== undefined) url.searchParams.set('offset', params.offset.toString());
|
||||
if (params.mode) url.searchParams.set('mode', params.mode);
|
||||
|
||||
console.log(`DeepNLP API Request (attempt ${retryCount + 1}):`, url.toString());
|
||||
// Adicionar parâmetros de busca
|
||||
if (params.query) url.searchParams.set("query", params.query);
|
||||
if (params.category) url.searchParams.set("category", params.category);
|
||||
if (params.id) url.searchParams.set("id", params.id);
|
||||
if (params.page_id !== undefined)
|
||||
url.searchParams.set("page_id", params.page_id.toString());
|
||||
if (params.count_per_page !== undefined)
|
||||
url.searchParams.set(
|
||||
"count_per_page",
|
||||
params.count_per_page.toString()
|
||||
);
|
||||
if (params.offset !== undefined)
|
||||
url.searchParams.set("offset", params.offset.toString());
|
||||
if (params.mode) url.searchParams.set("mode", params.mode);
|
||||
|
||||
console.log(
|
||||
`DeepNLP API Request (attempt ${retryCount + 1}):`,
|
||||
url.toString()
|
||||
);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'Ollahub-MCP-Client/1.0'
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0",
|
||||
},
|
||||
// Timeout de 10 segundos
|
||||
signal: AbortSignal.timeout(10000)
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`DeepNLP API error: ${response.status} ${response.statusText}`);
|
||||
throw new Error(
|
||||
`DeepNLP API error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('DeepNLP API Response:', data);
|
||||
console.log("DeepNLP API Response:", data);
|
||||
|
||||
// Salvar no cache
|
||||
MCPCache.setWithDataType(cacheKey, data, 'search');
|
||||
MCPCache.setWithDataType(cacheKey, data, "search");
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error calling DeepNLP API (attempt ${retryCount + 1}):`, error);
|
||||
|
||||
console.error(
|
||||
`Error calling DeepNLP API (attempt ${retryCount + 1}):`,
|
||||
error
|
||||
);
|
||||
|
||||
// Se ainda temos tentativas restantes, tentar novamente com backoff exponencial
|
||||
if (retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount); // Backoff exponencial
|
||||
console.log(`Retrying in ${delay}ms...`);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return this.searchMCPs(params, retryCount + 1);
|
||||
}
|
||||
|
||||
|
||||
// Se esgotamos as tentativas, lançar erro
|
||||
throw new Error(`Failed to fetch MCPs after ${maxRetries + 1} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
throw new Error(
|
||||
`Failed to fetch MCPs after ${maxRetries + 1} attempts: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter configuração completa de um servidor específico
|
||||
*/
|
||||
static async getServerConfig(id: string, retryCount = 0): Promise<MCPServerConfig> {
|
||||
static async getServerConfig(
|
||||
id: string,
|
||||
retryCount = 0
|
||||
): Promise<MCPServerConfig> {
|
||||
const maxRetries = 3;
|
||||
const baseDelay = 1000;
|
||||
|
||||
@@ -111,52 +135,66 @@ export class DeepNLPService {
|
||||
// Verificar cache primeiro
|
||||
const cacheKey = MCPCache.generateServerConfigKey(id);
|
||||
const cachedResult = MCPCache.get(cacheKey);
|
||||
|
||||
|
||||
if (cachedResult) {
|
||||
console.log('Returning cached server config');
|
||||
console.log("Returning cached server config");
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const [owner, repo] = id.split('/');
|
||||
const [owner, repo] = id.split("/");
|
||||
if (!owner || !repo) {
|
||||
throw new Error(`Invalid MCP ID format: ${id}. Expected format: owner/repo`);
|
||||
throw new Error(
|
||||
`Invalid MCP ID format: ${id}. Expected format: owner/repo`
|
||||
);
|
||||
}
|
||||
|
||||
const url = `${DEEPNLP_API_BASE}/server/${owner}/${repo}`;
|
||||
console.log(`DeepNLP Server Config Request (attempt ${retryCount + 1}):`, url);
|
||||
console.log(
|
||||
`DeepNLP Server Config Request (attempt ${retryCount + 1}):`,
|
||||
url
|
||||
);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'Ollahub-MCP-Client/1.0'
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0",
|
||||
},
|
||||
signal: AbortSignal.timeout(10000)
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`DeepNLP Server Config API error: ${response.status} ${response.statusText}`);
|
||||
throw new Error(
|
||||
`DeepNLP Server Config API error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('DeepNLP Server Config Response:', data);
|
||||
console.log("DeepNLP Server Config Response:", data);
|
||||
|
||||
// Salvar no cache
|
||||
MCPCache.setWithDataType(cacheKey, data, 'config');
|
||||
MCPCache.setWithDataType(cacheKey, data, "config");
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Error getting server config from DeepNLP (attempt ${retryCount + 1}):`, error);
|
||||
|
||||
console.error(
|
||||
`Error getting server config from DeepNLP (attempt ${retryCount + 1}):`,
|
||||
error
|
||||
);
|
||||
|
||||
if (retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount);
|
||||
console.log(`Retrying server config in ${delay}ms...`);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return this.getServerConfig(id, retryCount + 1);
|
||||
}
|
||||
|
||||
throw new Error(`Failed to get server config after ${maxRetries + 1} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
|
||||
throw new Error(
|
||||
`Failed to get server config after ${maxRetries + 1} attempts: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,54 +209,68 @@ export class DeepNLPService {
|
||||
// Verificar cache primeiro
|
||||
const cacheKey = MCPCache.generateServerToolsKey(id);
|
||||
const cachedResult = MCPCache.get(cacheKey);
|
||||
|
||||
|
||||
if (cachedResult) {
|
||||
console.log('Returning cached server tools');
|
||||
console.log("Returning cached server tools");
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
const [owner, repo] = id.split('/');
|
||||
const [owner, repo] = id.split("/");
|
||||
if (!owner || !repo) {
|
||||
throw new Error(`Invalid MCP ID format: ${id}. Expected format: owner/repo`);
|
||||
throw new Error(
|
||||
`Invalid MCP ID format: ${id}. Expected format: owner/repo`
|
||||
);
|
||||
}
|
||||
|
||||
const url = `${DEEPNLP_API_BASE}/tools/${owner}/${repo}`;
|
||||
console.log(`DeepNLP Server Tools Request (attempt ${retryCount + 1}):`, url);
|
||||
console.log(
|
||||
`DeepNLP Server Tools Request (attempt ${retryCount + 1}):`,
|
||||
url
|
||||
);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'Ollahub-MCP-Client/1.0'
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0",
|
||||
},
|
||||
signal: AbortSignal.timeout(10000)
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`DeepNLP Server Tools API error: ${response.status} ${response.statusText}`);
|
||||
throw new Error(
|
||||
`DeepNLP Server Tools API error: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('DeepNLP Server Tools Response:', data);
|
||||
console.log("DeepNLP Server Tools Response:", data);
|
||||
|
||||
const tools = data.tools || [];
|
||||
|
||||
// Salvar no cache
|
||||
MCPCache.setWithDataType(cacheKey, tools, 'tools');
|
||||
MCPCache.setWithDataType(cacheKey, tools, "tools");
|
||||
|
||||
return tools;
|
||||
} catch (error) {
|
||||
console.error(`Error getting server tools from DeepNLP (attempt ${retryCount + 1}):`, error);
|
||||
|
||||
console.error(
|
||||
`Error getting server tools from DeepNLP (attempt ${retryCount + 1}):`,
|
||||
error
|
||||
);
|
||||
|
||||
if (retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount);
|
||||
console.log(`Retrying server tools in ${delay}ms...`);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return this.getServerTools(id, retryCount + 1);
|
||||
}
|
||||
|
||||
throw new Error(`Failed to get server tools after ${maxRetries + 1} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
|
||||
throw new Error(
|
||||
`Failed to get server tools after ${maxRetries + 1} attempts: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,44 +278,45 @@ export class DeepNLPService {
|
||||
* Transformar resposta DeepNLP para formato interno MCPProvider
|
||||
*/
|
||||
static transformToMCPProvider(deepnlpItem: any): MCPProvider {
|
||||
const category = this.mapCategory(deepnlpItem.subfield || 'other');
|
||||
|
||||
const category = this.mapCategory(deepnlpItem.subfield || "other");
|
||||
|
||||
return {
|
||||
id: deepnlpItem.id || '',
|
||||
name: deepnlpItem.content_name || '',
|
||||
author: deepnlpItem.publisher_id || '',
|
||||
description: deepnlpItem.description || '',
|
||||
version: '1.0.0', // DeepNLP não fornece versão específica
|
||||
id: deepnlpItem.id || "",
|
||||
name: deepnlpItem.content_name || "",
|
||||
author: deepnlpItem.publisher_id || "",
|
||||
description: deepnlpItem.description || "",
|
||||
version: "1.0.0", // DeepNLP não fornece versão específica
|
||||
category,
|
||||
tags: deepnlpItem.content_tag_list ? deepnlpItem.content_tag_list.split(',').map((tag: string) => tag.trim()) : [],
|
||||
rating: parseFloat(deepnlpItem.rating || '0'),
|
||||
totalRatings: parseInt(deepnlpItem.review_cnt || '0'),
|
||||
downloads: parseInt(deepnlpItem.review_cnt || '0') * 10, // Estimativa baseada em reviews
|
||||
iconUrl: deepnlpItem.thumbnail_picture,
|
||||
screenshotUrls: [],
|
||||
capabilities: deepnlpItem.ext_info?.tools || [],
|
||||
tags: deepnlpItem.content_tag_list
|
||||
? deepnlpItem.content_tag_list
|
||||
.split(",")
|
||||
.map((tag: string) => tag.trim())
|
||||
: [],
|
||||
rating: parseFloat(deepnlpItem.rating || "0"),
|
||||
totalRatings: parseInt(deepnlpItem.review_cnt || "0"),
|
||||
repository: deepnlpItem.website,
|
||||
homepage: deepnlpItem.website,
|
||||
installed: false, // Será verificado localmente
|
||||
subfield: deepnlpItem.subfield,
|
||||
field: deepnlpItem.field,
|
||||
config: deepnlpItem.config,
|
||||
tools: deepnlpItem.ext_info?.tools || []
|
||||
config: deepnlpItem.config || [],
|
||||
tools: deepnlpItem.ext_info?.tools || [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapear categoria DeepNLP para categoria interna
|
||||
*/
|
||||
static mapCategory(subfield: string): MCPProvider['category'] {
|
||||
return this.reverseCategoryMap[subfield?.toLowerCase()] || 'other';
|
||||
static mapCategory(subfield: string): MCPProvider["category"] {
|
||||
return this.reverseCategoryMap[subfield?.toLowerCase()] || "other";
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter mapeamento de categoria interna para DeepNLP
|
||||
*/
|
||||
static getCategoryMapping(internalCategory: string): string | undefined {
|
||||
const mappings = this.categoryMap[internalCategory as keyof typeof this.categoryMap];
|
||||
const mappings =
|
||||
this.categoryMap[internalCategory as keyof typeof this.categoryMap];
|
||||
return mappings?.[0]; // Retorna a primeira categoria DeepNLP correspondente
|
||||
}
|
||||
|
||||
@@ -273,15 +326,15 @@ export class DeepNLPService {
|
||||
static async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${DEEPNLP_API_BASE}?count_per_page=1`, {
|
||||
method: 'GET',
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'Ollahub-MCP-Client/1.0'
|
||||
}
|
||||
Accept: "application/json",
|
||||
"User-Agent": "Ollahub-MCP-Client/1.0",
|
||||
},
|
||||
});
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('DeepNLP API health check failed:', error);
|
||||
console.error("DeepNLP API health check failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+58
-123
@@ -1,4 +1,4 @@
|
||||
import { DeepNLPService } from "./deepnlp";
|
||||
import { PulseMCPService } from "./pulsemcp";
|
||||
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
|
||||
|
||||
export interface SyncProgress {
|
||||
@@ -131,143 +131,78 @@ export class MCPSyncService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Download paginado de todas as páginas - SEM LIMITE ARTIFICIAL
|
||||
* Download completo de todos os servidores PulseMCP (uma única chamada)
|
||||
*/
|
||||
private static async downloadAllPages(
|
||||
onProgress?: (progress: SyncProgress) => void
|
||||
): Promise<any[]> {
|
||||
const allMCPs: any[] = [];
|
||||
let page = 0;
|
||||
let COUNT_PER_PAGE = 100; // Começar com 100
|
||||
let totalHits = 0;
|
||||
let consecutiveEmptyPages = 0; // Contador para páginas vazias consecutivas
|
||||
try {
|
||||
console.log("📡 Starting complete download from PulseMCP...");
|
||||
|
||||
console.log("📡 Starting unlimited paginated download...");
|
||||
// Notificar início
|
||||
this.currentProgress = {
|
||||
current: 0,
|
||||
total: 0,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
while (!this.abortController?.signal.aborted) {
|
||||
try {
|
||||
const params = {
|
||||
mode: "list" as const,
|
||||
page_id: page,
|
||||
count_per_page: COUNT_PER_PAGE,
|
||||
};
|
||||
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(
|
||||
`📄 Downloading page ${
|
||||
page + 1
|
||||
} (${COUNT_PER_PAGE} items per page)...`
|
||||
`💾 Saving batch ${i + 1}/${totalBatches} (${batch.length} items)...`
|
||||
);
|
||||
MCPCacheRepository.saveBatch(batch);
|
||||
|
||||
const response = await DeepNLPService.searchMCPs(params);
|
||||
|
||||
// Se não há itens na resposta, incrementar contador de páginas vazias
|
||||
if (!response.items || response.items.length === 0) {
|
||||
consecutiveEmptyPages++;
|
||||
console.log(
|
||||
`⚠️ Empty page ${
|
||||
page + 1
|
||||
} (${consecutiveEmptyPages} consecutive empty pages)`
|
||||
);
|
||||
|
||||
// Se temos 3 páginas vazias consecutivas, parar o download
|
||||
if (consecutiveEmptyPages >= 3) {
|
||||
console.log(
|
||||
"🛑 Stopping download: 3 consecutive empty pages detected"
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
page++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset contador de páginas vazias se encontramos dados
|
||||
consecutiveEmptyPages = 0;
|
||||
allMCPs.push(...response.items);
|
||||
|
||||
// Atualizar total hits na primeira página
|
||||
if (page === 0) {
|
||||
totalHits = response.total_hits;
|
||||
console.log(`📊 Total MCPs available: ${totalHits}`);
|
||||
}
|
||||
|
||||
// Notificar progresso
|
||||
this.currentProgress = {
|
||||
current: allMCPs.length,
|
||||
total: totalHits,
|
||||
page: page + 1,
|
||||
totalPages:
|
||||
totalHits > 0 ? Math.ceil(totalHits / COUNT_PER_PAGE) : page + 1,
|
||||
};
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(this.currentProgress);
|
||||
}
|
||||
|
||||
// Incrementar página
|
||||
page++;
|
||||
|
||||
// Salvar em batch a cada 500 itens para evitar bloqueio
|
||||
if (allMCPs.length % 500 === 0) {
|
||||
console.log(`💾 Saving batch of 500 MCPs to database...`);
|
||||
MCPCacheRepository.saveBatch(allMCPs.slice(-500));
|
||||
}
|
||||
|
||||
// Pequena pausa entre requisições para não sobrecarregar a API
|
||||
// Pequena pausa entre batches
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Aumentar tamanho da página progressivamente para otimizar
|
||||
if (page === 5 && COUNT_PER_PAGE === 100) {
|
||||
COUNT_PER_PAGE = 500;
|
||||
console.log("🚀 Increasing page size to 500 for faster download");
|
||||
} else if (page === 20 && COUNT_PER_PAGE === 500) {
|
||||
COUNT_PER_PAGE = 1000;
|
||||
console.log("🚀 Increasing page size to 1000 for maximum efficiency");
|
||||
}
|
||||
|
||||
// Log de progresso a cada 10 páginas
|
||||
if (page % 10 === 0) {
|
||||
console.log(
|
||||
`📈 Progress: ${allMCPs.length} MCPs downloaded (page ${page})`
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`❌ Error downloading page ${page + 1}:`, error);
|
||||
|
||||
// Se for erro de abort, parar
|
||||
if (this.abortController?.signal.aborted) {
|
||||
console.log("🛑 Download cancelled");
|
||||
break;
|
||||
}
|
||||
|
||||
// Se for erro de rede, tentar continuar
|
||||
if (
|
||||
error.message.includes("fetch") ||
|
||||
error.message.includes("network")
|
||||
) {
|
||||
console.log("🔄 Network error, retrying in 2 seconds...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Para outros erros, parar
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Salvar último batch se houver
|
||||
if (allMCPs.length > 0) {
|
||||
const remainingItems = allMCPs.length % 500;
|
||||
if (remainingItems > 0) {
|
||||
console.log(`💾 Saving final batch of ${remainingItems} MCPs...`);
|
||||
MCPCacheRepository.saveBatch(allMCPs.slice(-remainingItems));
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ Download completed: ${allMCPs.length} total MCPs downloaded`
|
||||
);
|
||||
return allMCPs;
|
||||
// Para outros erros, lançar
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
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)
|
||||
*/
|
||||
static async getAllServers(): Promise<PulseMCPResponse> {
|
||||
try {
|
||||
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) {
|
||||
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) {
|
||||
console.error("❌ Error calling PulseMCP API:", error);
|
||||
throw new Error(
|
||||
`Failed to fetch MCPs: ${
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
-11
@@ -18,10 +18,6 @@ export interface MCPProvider {
|
||||
tags: string[]; // mapeado de content_tag_list
|
||||
rating: number; // já existe na API
|
||||
totalRatings: number; // mapeado de review_cnt
|
||||
downloads: number; // opcional, calcular baseado em reviews
|
||||
iconUrl?: string; // mapeado de thumbnail_picture
|
||||
screenshotUrls?: string[];
|
||||
capabilities: string[]; // mapeado de ext_info.tools
|
||||
repository?: string; // mapeado de website
|
||||
homepage?: string;
|
||||
installed: boolean; // verificar localmente
|
||||
@@ -81,14 +77,48 @@ export interface MCPServerConfig {
|
||||
export interface MCPSearchParams {
|
||||
category?: string;
|
||||
search?: string;
|
||||
sort?:
|
||||
| "rating"
|
||||
| "downloads"
|
||||
| "name"
|
||||
| "recent"
|
||||
| "total_ratings"
|
||||
| "updated_at";
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/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 };
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/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);
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { MCPCacheRepository } from "../database/repositories/mcp-cache";
|
||||
import { MCPSyncService } from "../lib/services/mcp-sync";
|
||||
import { PulseMCPService } from "../lib/services/pulsemcp";
|
||||
|
||||
async function testSimpleSync() {
|
||||
console.log("🔄 Testando sincronização simples com PulseMCP...");
|
||||
|
||||
try {
|
||||
// 1. Verificar se PulseMCP API está funcionando
|
||||
console.log("🔍 Verificando conectividade com PulseMCP API...");
|
||||
const isHealthy = await PulseMCPService.checkHealth();
|
||||
|
||||
if (!isHealthy) {
|
||||
console.error("❌ PulseMCP API não está acessível");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("✅ PulseMCP API está funcionando");
|
||||
|
||||
// 2. Limpar cache antigo
|
||||
console.log("🗑️ Limpando cache antigo...");
|
||||
MCPCacheRepository.clearCache();
|
||||
console.log("✅ Cache antigo limpo");
|
||||
|
||||
// 3. Testar chamada direta à API
|
||||
console.log("📡 Testando chamada direta à API PulseMCP...");
|
||||
const response = await PulseMCPService.getAllServers();
|
||||
console.log(
|
||||
`✅ API retornou ${response.servers.length} servidores (total: ${response.total_count})`
|
||||
);
|
||||
|
||||
// 4. Salvar alguns servidores no banco para teste
|
||||
console.log("💾 Salvando primeiros 10 servidores no banco...");
|
||||
const testBatch = response.servers.slice(0, 10);
|
||||
MCPCacheRepository.saveBatch(testBatch);
|
||||
console.log("✅ Servidores salvos no banco");
|
||||
|
||||
// 5. Verificar se foram salvos corretamente
|
||||
console.log("🔍 Verificando dados salvos...");
|
||||
const savedServers = MCPCacheRepository.search({ limit: 10 });
|
||||
console.log(`📋 Encontrados ${savedServers.length} servidores no banco:`);
|
||||
|
||||
savedServers.forEach((server, index) => {
|
||||
console.log(
|
||||
` ${index + 1}. ${server.content_name} (${server.category})`
|
||||
);
|
||||
});
|
||||
|
||||
// 6. Testar sincronização completa
|
||||
console.log("🚀 Iniciando sincronização completa...");
|
||||
const result = await MCPSyncService.syncAll((progress) => {
|
||||
const percentage =
|
||||
progress.total > 0
|
||||
? Math.round((progress.current / progress.total) * 100)
|
||||
: 0;
|
||||
console.log(
|
||||
`📊 Progresso: ${progress.current}/${progress.total} (${percentage}%)`
|
||||
);
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ Sincronização concluída com sucesso!`);
|
||||
console.log(`📊 Total de servidores baixados: ${result.totalDownloaded}`);
|
||||
console.log(`⏱️ Tempo total: ${Math.round(result.duration / 1000)}s`);
|
||||
|
||||
// 7. Verificar estatísticas finais
|
||||
const stats = MCPCacheRepository.getStats();
|
||||
console.log(`📈 Estatísticas finais do cache:`);
|
||||
console.log(` - Total de itens: ${stats.totalItems}`);
|
||||
console.log(
|
||||
` - Última sincronização: ${
|
||||
stats.lastSync ? new Date(stats.lastSync).toLocaleString() : "Nunca"
|
||||
}`
|
||||
);
|
||||
console.log(` - Cache válido: ${stats.isValid ? "Sim" : "Não"}`);
|
||||
} else {
|
||||
console.error("❌ Sincronização falhou:");
|
||||
result.errors.forEach((error) => console.error(` - ${error}`));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Erro durante teste:", error);
|
||||
}
|
||||
}
|
||||
|
||||
testSimpleSync();
|
||||
|
||||
export { testSimpleSync };
|
||||
Reference in new issue
Block a user