Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
custom-registry-client.ts7.35 kB
/** * Custom MCP Registry Client * Connects to our custom registry at api.mcps.portel.dev */ import { logger } from '../utils/logger.js'; export interface CustomRegistryMCP { id: string; name: string; displayName: string; description: string; version: string; author: { name: string; }; repository: { url: string; source: string; }; homepage?: string; keywords: string[]; license: string; installCommand: string; runtimeHint: string; transport: { type: string; }; createdAt: string; updatedAt: string; verified: boolean; stats: { downloads: number; stars: number; upvotes: number; downvotes: number; views: number; score: number; }; _meta?: any; // Metadata (e.g., Photon detection) } export interface CustomRegistrySearchOptions { /** Search query */ q?: string; /** Sort by: downloads, stars, name, recent */ sort?: 'downloads' | 'stars' | 'name' | 'recent'; /** Category filter */ category?: string; /** Only verified MCPs */ verified?: boolean; /** Maximum results */ limit?: number; } export interface CustomRegistryStats { totalMCPs: number; totalVotes: number; totalUsers: number; totalTags: number; verifiedMCPs: number; totalDownloads: number; } export class CustomRegistryClient { private baseURL = 'https://api.mcps.portel.dev'; private cache: Map<string, { data: any; timestamp: number }> = new Map(); private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes /** * Search for MCPs in the custom registry */ async search(options: CustomRegistrySearchOptions = {}): Promise<CustomRegistryMCP[]> { try { const params = new URLSearchParams(); if (options.q) params.set('q', options.q); if (options.sort) params.set('sort', options.sort); if (options.category) params.set('category', options.category); if (options.verified) params.set('verified', 'true'); if (options.limit) params.set('limit', options.limit.toString()); const endpoint = options.q ? '/search' : '/trending'; const url = `${this.baseURL}${endpoint}?${params}`; logger.debug(`Searching custom registry: ${url}`); const response = await fetch(url); if (!response.ok) { throw new Error(`Registry API error: ${response.status} ${response.statusText}`); } const result = await response.json(); let mcps = result.data || result; // Handle search API response format (nested mcps array) if (mcps && typeof mcps === 'object' && 'mcps' in mcps) { mcps = mcps.mcps; } // Ensure we return an array if (!Array.isArray(mcps)) { logger.warn('API returned non-array response, wrapping in array'); return mcps ? [mcps] : []; } logger.debug(`Found ${mcps.length} MCPs from custom registry`); return mcps; } catch (error: any) { logger.error(`Custom registry search failed: ${error.message}`); throw new Error(`Failed to search custom registry: ${error.message}`); } } /** * Get trending MCPs */ async getTrending(limit: number = 10): Promise<CustomRegistryMCP[]> { try { const cacheKey = `trending:${limit}`; const cached = this.getFromCache(cacheKey); if (cached) return cached; const url = `${this.baseURL}/trending?limit=${limit}`; const response = await fetch(url); if (!response.ok) { throw new Error(`Registry API error: ${response.status}`); } const result = await response.json(); const mcps = result.data || result; this.setCache(cacheKey, mcps); return mcps; } catch (error: any) { logger.error(`Failed to get trending: ${error.message}`); throw new Error(`Failed to get trending MCPs: ${error.message}`); } } /** * Get specific MCP by ID */ async getMCP(id: string): Promise<CustomRegistryMCP> { try { const cacheKey = `mcp:${id}`; const cached = this.getFromCache(cacheKey); if (cached) return cached; const url = `${this.baseURL}/mcp?id=${encodeURIComponent(id)}`; const response = await fetch(url); if (!response.ok) { throw new Error(`MCP not found: ${id}`); } const result = await response.json(); const mcp = result.data || result; this.setCache(cacheKey, mcp); return mcp; } catch (error: any) { logger.error(`Failed to get MCP ${id}: ${error.message}`); throw new Error(`Failed to get MCP: ${error.message}`); } } /** * Get registry statistics */ async getStats(): Promise<CustomRegistryStats> { try { const cacheKey = 'stats'; const cached = this.getFromCache(cacheKey); if (cached) return cached; const url = `${this.baseURL}/stats`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to get stats: ${response.status}`); } const result = await response.json(); const stats = result.data || result; this.setCache(cacheKey, stats); return stats; } catch (error: any) { logger.error(`Failed to get stats: ${error.message}`); throw new Error(`Failed to get registry stats: ${error.message}`); } } /** * Vote on an MCP */ async vote(id: string, vote: 'up' | 'down'): Promise<void> { try { const url = `${this.baseURL}/vote`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, vote }) }); if (!response.ok) { throw new Error(`Failed to vote: ${response.status}`); } // Clear cache for this MCP this.cache.delete(`mcp:${id}`); } catch (error: any) { logger.error(`Failed to vote: ${error.message}`); throw new Error(`Failed to vote on MCP: ${error.message}`); } } /** * Search and format for user selection */ async searchForSelection(query: string, limit: number = 20): Promise<Array<{ number: number; id: string; name: string; displayName: string; description: string; version: string; verified: boolean; downloads: number; stars: number; score: number; installCommand: string; }>> { // Use default relevance sorting (no sort parameter) to get best matches first // Sorting by downloads can put less relevant results first when downloads are tied const results = await this.search({ q: query, limit }); return results.map((mcp, index) => ({ number: index + 1, id: mcp.id, name: mcp.name, displayName: mcp.displayName, description: mcp.description, version: mcp.version, verified: mcp.verified, downloads: mcp.stats?.downloads || 0, stars: mcp.stats?.stars || 0, score: mcp.stats?.score || 0, installCommand: mcp.installCommand })); } private getFromCache(key: string): any | null { const cached = this.cache.get(key); if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { return cached.data; } return null; } private setCache(key: string, data: any): void { this.cache.set(key, { data, timestamp: Date.now() }); } clearCache(): void { this.cache.clear(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/portel-dev/ncp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server