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:
Evandro Fonseca Junior committed 2025-10-28 20:19:00 -03:00
1 parent ce010d39ee
commit 1042f2136b
15 files changed
+1237 -398

No files matched your search

+161 -108
View File
@@ -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;
}
}