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>
64 lines
2.1 KiB
TypeScript
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));
|
|
}
|