Files
mcpctl/src/cli/src/registry/ranking.ts
Michal 386029d052 feat: implement MCP registry client with multi-source search
Add registry client that queries Official, Glama, and Smithery MCP
registries with caching, request deduplication, retry logic, and
result ranking/dedup. Includes 53 tests covering all components.

Also fix null priority values in cancelled tasks (19-21) that broke
Task Master, and add new tasks 25-27 for registry completion and
CLI discover/install commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 03:46:14 +00:00

64 lines
2.1 KiB
TypeScript

import type { RegistryServer } from './types.js';
const WEIGHT_RELEVANCE = 0.4;
const WEIGHT_POPULARITY = 0.3;
const WEIGHT_VERIFIED = 0.2;
const WEIGHT_RECENCY = 0.1;
function textRelevance(server: RegistryServer, query: string): number {
const q = query.toLowerCase();
const name = server.name.toLowerCase();
const desc = server.description.toLowerCase();
// Exact name match
if (name === q) return 1.0;
// Name starts with query
if (name.startsWith(q)) return 0.9;
// Name contains query
if (name.includes(q)) return 0.7;
// Description contains query
if (desc.includes(q)) return 0.4;
// Word-level matching
const queryWords = q.split(/\s+/);
const matchCount = queryWords.filter(
(w) => name.includes(w) || desc.includes(w),
).length;
return queryWords.length > 0 ? (matchCount / queryWords.length) * 0.3 : 0;
}
function popularityScore(server: RegistryServer): number {
// Normalize to 0-1 range; use log scale since popularity can vary hugely
if (server.popularityScore <= 0) return 0;
// Log scale: log10(1) = 0, log10(10000) ≈ 4 → normalize to 0-1 with cap at 100k
return Math.min(Math.log10(server.popularityScore + 1) / 5, 1.0);
}
function verifiedScore(server: RegistryServer): number {
return server.verified ? 1.0 : 0;
}
function recencyScore(server: RegistryServer): number {
if (server.lastUpdated === undefined) return 0.5; // Unknown = middle score
const ageMs = Date.now() - server.lastUpdated.getTime();
const ageDays = ageMs / (1000 * 60 * 60 * 24);
// Less than 30 days = 1.0, decays to 0 at 365 days
return Math.max(0, 1 - ageDays / 365);
}
function computeScore(server: RegistryServer, query: string): number {
return (
WEIGHT_RELEVANCE * textRelevance(server, query) +
WEIGHT_POPULARITY * popularityScore(server) +
WEIGHT_VERIFIED * verifiedScore(server) +
WEIGHT_RECENCY * recencyScore(server)
);
}
export function rankResults(
results: RegistryServer[],
query: string,
): RegistryServer[] {
return [...results].sort((a, b) => computeScore(b, query) - computeScore(a, query));
}