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>
107 lines
3.8 KiB
TypeScript
107 lines
3.8 KiB
TypeScript
import { RegistrySource } from '../base.js';
|
|
import {
|
|
OfficialRegistryResponseSchema,
|
|
sanitizeString,
|
|
type OfficialServerEntry,
|
|
type RegistryServer,
|
|
} from '../types.js';
|
|
import { withRetry } from '../retry.js';
|
|
|
|
const BASE_URL = 'https://registry.modelcontextprotocol.io/v0/servers';
|
|
|
|
export class OfficialRegistrySource extends RegistrySource {
|
|
readonly name = 'official' as const;
|
|
|
|
async search(query: string, limit: number): Promise<RegistryServer[]> {
|
|
const results: RegistryServer[] = [];
|
|
let cursor: string | null | undefined;
|
|
|
|
while (results.length < limit) {
|
|
const url = new URL(BASE_URL);
|
|
url.searchParams.set('search', query);
|
|
url.searchParams.set('limit', String(Math.min(limit - results.length, 100)));
|
|
if (cursor !== undefined && cursor !== null) {
|
|
url.searchParams.set('cursor', cursor);
|
|
}
|
|
|
|
const response = await withRetry(() => fetch(url.toString()));
|
|
if (!response.ok) {
|
|
throw new Error(`Official registry returned ${String(response.status)}`);
|
|
}
|
|
|
|
const raw: unknown = await response.json();
|
|
const parsed = OfficialRegistryResponseSchema.parse(raw);
|
|
|
|
for (const entry of parsed.servers) {
|
|
results.push(this.normalizeResult(entry));
|
|
}
|
|
|
|
cursor = parsed.metadata?.nextCursor;
|
|
if (cursor === null || cursor === undefined || parsed.servers.length === 0) break;
|
|
}
|
|
|
|
return results.slice(0, limit);
|
|
}
|
|
|
|
protected normalizeResult(raw: unknown): RegistryServer {
|
|
const entry = raw as OfficialServerEntry;
|
|
const server = entry.server;
|
|
|
|
// Extract env vars from packages
|
|
const envTemplate = server.packages.flatMap((pkg: { environmentVariables: Array<{ name: string; description?: string; isSecret?: boolean }> }) =>
|
|
pkg.environmentVariables.map((ev: { name: string; description?: string; isSecret?: boolean }) => ({
|
|
name: ev.name,
|
|
description: sanitizeString(ev.description ?? ''),
|
|
isSecret: ev.isSecret ?? false,
|
|
})),
|
|
);
|
|
|
|
// Determine transport from packages or remotes
|
|
let transport: RegistryServer['transport'] = 'stdio';
|
|
if (server.packages.length > 0) {
|
|
const pkgTransport = server.packages[0]?.transport?.type;
|
|
if (pkgTransport === 'stdio') transport = 'stdio';
|
|
}
|
|
if (server.remotes.length > 0) {
|
|
const remoteType = server.remotes[0]?.type;
|
|
if (remoteType === 'sse') transport = 'sse';
|
|
else if (remoteType === 'streamable-http') transport = 'streamable-http';
|
|
}
|
|
|
|
// Extract npm package identifier
|
|
const npmPkg = server.packages.find((p: { registryType: string }) => p.registryType === 'npm');
|
|
const dockerPkg = server.packages.find((p: { registryType: string }) => p.registryType === 'oci');
|
|
|
|
// Extract dates from _meta
|
|
const meta = entry._meta as Record<string, Record<string, unknown>> | undefined;
|
|
const officialMeta = meta?.['io.modelcontextprotocol.registry/official'];
|
|
const updatedAt = officialMeta?.['updatedAt'];
|
|
|
|
const packages: RegistryServer['packages'] = {};
|
|
if (npmPkg !== undefined) {
|
|
packages.npm = npmPkg.identifier;
|
|
}
|
|
if (dockerPkg !== undefined) {
|
|
packages.docker = dockerPkg.identifier;
|
|
}
|
|
|
|
const result: RegistryServer = {
|
|
name: sanitizeString(server.title ?? server.name),
|
|
description: sanitizeString(server.description),
|
|
packages,
|
|
envTemplate,
|
|
transport,
|
|
popularityScore: 0, // Official registry has no popularity data
|
|
verified: false, // Official registry has no verified badges
|
|
sourceRegistry: 'official',
|
|
};
|
|
if (server.repository?.url !== undefined) {
|
|
result.repositoryUrl = server.repository.url;
|
|
}
|
|
if (typeof updatedAt === 'string') {
|
|
result.lastUpdated = new Date(updatedAt);
|
|
}
|
|
return result;
|
|
}
|
|
}
|