Skip to main content
Glama
naoto24kawa

Composer Package README MCP Server

by naoto24kawa
packagist-api.ts8.4 kB
import { logger } from '../utils/logger.js'; import { handleApiError, handleHttpError, withRetry } from '../utils/error-handler.js'; import { VersionResolver } from './version-resolver.js'; import { API_CONSTANTS } from '../utils/constants.js'; import { cache, createCacheKey } from './cache.js'; import { PackagistPackageInfo, PackagistVersionInfo, PackagistSearchResponse, PackagistStatsResponse, VersionNotFoundError, } from '../types/index.js'; export class PackagistApiClient { private readonly baseUrl = 'https://packagist.org/packages'; private readonly searchUrl = 'https://packagist.org/search.json'; private readonly statsUrl = 'https://packagist.org/packages'; private readonly timeout: number; constructor(timeout?: number) { this.timeout = timeout || API_CONSTANTS.DEFAULT_TIMEOUT_MS; } async checkPackageExists(packageName: string): Promise<boolean> { const url = `${this.baseUrl}/${encodeURIComponent(packageName)}.json`; return withRetry(async () => { logger.debug(`Checking package existence: ${packageName}`); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { method: 'HEAD', signal: controller.signal, headers: { 'User-Agent': API_CONSTANTS.USER_AGENT, }, }); const exists = response.ok; logger.debug(`Package existence check: ${packageName} - ${exists ? 'exists' : 'not found'}`); return exists; } catch (error) { if ((error as Error).name === 'AbortError') { logger.warn(`Package existence check timeout: ${packageName}`); return false; } logger.warn(`Package existence check failed: ${packageName}`, { error }); return false; } finally { clearTimeout(timeoutId); } }, 3, 1000, `packagist checkPackageExists(${packageName})`); } async getPackageInfo(packageName: string): Promise<PackagistPackageInfo> { // Check cache first const cacheKey = createCacheKey.packageInfo(packageName, 'latest'); const cachedResult = cache.get<PackagistPackageInfo>(cacheKey); if (cachedResult) { logger.debug(`Cache hit for package info: ${packageName}`); return cachedResult; } const url = `${this.baseUrl}/${encodeURIComponent(packageName)}.json`; return withRetry(async () => { logger.debug(`Fetching package info: ${packageName}`); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { signal: controller.signal, headers: { 'Accept': 'application/json', 'User-Agent': API_CONSTANTS.USER_AGENT, }, }); if (!response.ok) { handleHttpError(response.status, response, `packagist for package ${packageName}`); } const data = await response.json() as { package: PackagistPackageInfo }; // Cache the result cache.set(cacheKey, data.package, 30 * 60 * 1000); // 30 minutes TTL logger.debug(`Successfully fetched package info: ${packageName}`); return data.package; } catch (error) { if ((error as Error).name === 'AbortError') { handleApiError(new Error('Request timeout'), `packagist for package ${packageName}`); } handleApiError(error, `packagist for package ${packageName}`); } finally { clearTimeout(timeoutId); } }, 3, 1000, `packagist getPackageInfo(${packageName})`); } async getVersionInfo(packageName: string, version: string): Promise<PackagistVersionInfo> { const packageInfo = await this.getPackageInfo(packageName); // Resolve version alias using dedicated resolver const actualVersion = VersionResolver.resolveVersion(packageInfo.versions, version); const versionInfo = packageInfo.versions[actualVersion]; if (!versionInfo) { throw new VersionNotFoundError(packageName, version); } return versionInfo; } async searchPackages( query: string, limit: number = 20, type?: string ): Promise<PackagistSearchResponse> { // Check cache first const cacheKey = createCacheKey.searchResults(query, limit, type); const cachedResult = cache.get<PackagistSearchResponse>(cacheKey); if (cachedResult) { logger.debug(`Cache hit for search: ${query}`); return cachedResult; } const params = new URLSearchParams({ q: query, per_page: limit.toString(), }); if (type) { params.append('type', type); } const url = `${this.searchUrl}?${params.toString()}`; return withRetry(async () => { logger.debug(`Searching packages: ${query} (limit: ${limit})`); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { signal: controller.signal, headers: { 'Accept': 'application/json', 'User-Agent': API_CONSTANTS.USER_AGENT, }, }); if (!response.ok) { handleHttpError(response.status, response, `packagist search for query ${query}`); } const data = await response.json() as PackagistSearchResponse; // Cache the result cache.set(cacheKey, data, 15 * 60 * 1000); // 15 minutes TTL for search results logger.debug(`Successfully searched packages: ${query}, found ${data.total} results`); return data; } catch (error) { if ((error as Error).name === 'AbortError') { handleApiError(new Error('Request timeout'), `packagist search for query ${query}`); } handleApiError(error, `packagist search for query ${query}`); } finally { clearTimeout(timeoutId); } }, 3, 1000, `packagist searchPackages(${query})`); } async getPackageStats(packageName: string): Promise<PackagistStatsResponse | null> { const url = `${this.statsUrl}/${encodeURIComponent(packageName)}/stats.json`; return withRetry(async () => { logger.debug(`Fetching package stats: ${packageName}`); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { signal: controller.signal, headers: { 'Accept': 'application/json', 'User-Agent': API_CONSTANTS.USER_AGENT, }, }); if (!response.ok) { if (response.status === 404) { // Package might not have stats, return null return null; } handleHttpError(response.status, response, `packagist stats for package ${packageName}`); } const data = await response.json() as PackagistStatsResponse; logger.debug(`Successfully fetched package stats: ${packageName}`); return data; } catch (error) { if ((error as Error).name === 'AbortError') { handleApiError(new Error('Request timeout'), `packagist stats for package ${packageName}`); } handleApiError(error, `packagist stats for package ${packageName}`); } finally { clearTimeout(timeoutId); } }, 3, 1000, `packagist getPackageStats(${packageName})`); } async getDownloadStats(packageName: string): Promise<{ total: number; monthly: number; daily: number; }> { try { const stats = await this.getPackageStats(packageName); if (!stats) { return { total: 0, monthly: 0, daily: 0, }; } return { total: stats.package.downloads.total || 0, monthly: stats.package.downloads.monthly || 0, daily: stats.package.downloads.daily || 0, }; } catch (error) { logger.warn(`Failed to fetch download stats for ${packageName}, using zeros`, { error }); return { total: 0, monthly: 0, daily: 0, }; } } } export const packagistApi = new PackagistApiClient();

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/naoto24kawa/composer-package-readme-mcp-server'

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