Named Package Comparison
compare_competitorsCompare known npm or PyPI packages side by side using live metadata to verify claims about release dates, maintenance activity, and license types. Use when you already have candidate packages and need evidence.
Instructions
Compare two or more exact package names side by side using live npm or PyPI metadata. Use this when you already know the candidate packages and need evidence for claims such as 'tool A is newer', 'tool B is still maintained', or 'these packages use different licenses'. Do not use it to discover unknown alternatives; use estimate_market for category search and market sizing instead. Registry responses are cached for 5 minutes.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| packages | Yes | Two to ten exact package names from the same registry, for example ['react', 'vue']. | |
| registry | No | Registry that all package names belong to. All compared packages must come from the same registry. | npm |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| packages | Yes | Package names that were requested for comparison. | |
| registry | Yes | Registry used for all comparisons. | |
| comparisons | Yes | Per-package lookup results returned in the same order as the input package list. |
Implementation Reference
- src/index.ts:849-970 (registration)Registration of the compare_competitors tool via this.server.registerTool with inputSchema, outputSchema, and the handler function.
this.server.registerTool( "compare_competitors", { title: "Named Package Comparison", description: "Compare two or more exact package names side by side using live npm or " + "PyPI metadata. Use this when you already know the candidate packages and " + "need evidence for claims such as 'tool A is newer', 'tool B is still " + "maintained', or 'these packages use different licenses'. It returns " + "per-package registry metadata in input order, with field availability " + "varying by registry. Missing or unpublished packages return found=false. " + "Do not use it to discover unknown alternatives, estimate market size, " + "or compare packages across different registries. Registry responses are " + "cached for 5 minutes.", inputSchema: { packages: z.array(z.string().trim().min(1)).min(2).max(10).describe( "Two to ten exact package names from the same registry, for example ['react', 'vue']. Use exact registry names, not search phrases or categories.", ), registry: z.enum(["npm", "pypi"]).default("npm").describe( "Registry that all package names belong to. All compared packages must come from the same registry, and returned metadata fields differ slightly between npm and PyPI.", ), }, outputSchema: { packages: z.array(z.string()).describe( "Package names that were requested for comparison.", ), registry: z.enum(["npm", "pypi"]).describe( "Registry used for all comparisons.", ), comparisons: z.array(z.object({ name: z.string().describe( "Package name that was looked up.", ), found: z.boolean().describe( "True when the registry lookup succeeded and returned package metadata.", ), description: z.string().describe( "Short package summary from the registry.", ).optional(), latestVersion: z.string().describe( "Latest package version known to the registry.", ).optional(), license: z.union([z.string(), z.null()]).describe( "Package license metadata when provided by the registry.", ).optional(), lastPublished: z.union([z.string(), z.null()]).describe( "Publish timestamp of the latest version when npm provides one.", ).optional(), created: z.union([z.string(), z.null()]).describe( "Package creation timestamp when npm provides one.", ).optional(), totalVersions: z.number().int().nonnegative().describe( "Number of published versions when npm metadata includes a version history.", ).optional(), author: z.string().describe( "Package author when PyPI metadata includes one.", ).optional(), keywords: z.array(z.string()).describe( "Registry keywords or tags associated with the package.", ).optional(), cached: z.boolean().describe( "True when the lookup came from the 5-minute cache.", ).optional(), error: z.string().describe( "Fetch error when registry metadata could not be retrieved for this package.", ).optional(), })).describe( "Per-package lookup results returned in the same order as the input package list. Some fields only exist for npm or only for PyPI, so consumers should treat absent fields as normal.", ), }, annotations: readOnlyNetworkToolAnnotations, }, async ({ packages, registry }) => { const comparisons = []; for (const pkg of packages) { if (registry === "npm") { const url = `https://registry.npmjs.org/${encodeURIComponent(pkg)}`; try { const { body, fromCache } = await cachedFetch(sql, url); const data = JSON.parse(body); const latest = data["dist-tags"]?.latest; const time = data.time || {}; comparisons.push({ name: pkg, found: true, description: (data.description || "").slice(0, 150), latestVersion: latest, license: data.license, lastPublished: time[latest] || null, created: time.created || null, totalVersions: Object.keys(data.versions || {}).length, keywords: (data.keywords || []).slice(0, 10), cached: fromCache, }); } catch { comparisons.push({ name: pkg, found: false, error: "fetch failed" }); } } else if (registry === "pypi") { const url = `https://pypi.org/pypi/${encodeURIComponent(pkg)}/json`; try { const { body, fromCache } = await cachedFetch(sql, url); const data = JSON.parse(body); const info = data.info || {}; comparisons.push({ name: pkg, found: true, description: (info.summary || "").slice(0, 150), latestVersion: info.version, license: info.license, author: info.author, keywords: info.keywords?.split(",").map((k: string) => k.trim()).slice(0, 10) || [], cached: fromCache, }); } catch { comparisons.push({ name: pkg, found: false, error: "fetch failed" }); } } } logUsage("compare_competitors", true); return structuredToolResult({ packages, registry, comparisons }); } ); - src/index.ts:921-970 (handler)Handler function for compare_competitors. Accepts packages (array of 2-10 strings) and registry ('npm' | 'pypi'), fetches metadata from npm registry API or PyPI JSON API for each package, and returns per-package comparison data (description, latestVersion, license, lastPublished, created, totalVersions, author, keywords, cached status).
async ({ packages, registry }) => { const comparisons = []; for (const pkg of packages) { if (registry === "npm") { const url = `https://registry.npmjs.org/${encodeURIComponent(pkg)}`; try { const { body, fromCache } = await cachedFetch(sql, url); const data = JSON.parse(body); const latest = data["dist-tags"]?.latest; const time = data.time || {}; comparisons.push({ name: pkg, found: true, description: (data.description || "").slice(0, 150), latestVersion: latest, license: data.license, lastPublished: time[latest] || null, created: time.created || null, totalVersions: Object.keys(data.versions || {}).length, keywords: (data.keywords || []).slice(0, 10), cached: fromCache, }); } catch { comparisons.push({ name: pkg, found: false, error: "fetch failed" }); } } else if (registry === "pypi") { const url = `https://pypi.org/pypi/${encodeURIComponent(pkg)}/json`; try { const { body, fromCache } = await cachedFetch(sql, url); const data = JSON.parse(body); const info = data.info || {}; comparisons.push({ name: pkg, found: true, description: (info.summary || "").slice(0, 150), latestVersion: info.version, license: info.license, author: info.author, keywords: info.keywords?.split(",").map((k: string) => k.trim()).slice(0, 10) || [], cached: fromCache, }); } catch { comparisons.push({ name: pkg, found: false, error: "fetch failed" }); } } } logUsage("compare_competitors", true); return structuredToolResult({ packages, registry, comparisons }); } ); - src/index.ts:863-919 (schema)Input/output Zod schemas for compare_competitors: input expects packages (2-10 strings) and registry ('npm'|'pypi'), output returns packages array, registry, and comparisons array with per-package metadata fields.
inputSchema: { packages: z.array(z.string().trim().min(1)).min(2).max(10).describe( "Two to ten exact package names from the same registry, for example ['react', 'vue']. Use exact registry names, not search phrases or categories.", ), registry: z.enum(["npm", "pypi"]).default("npm").describe( "Registry that all package names belong to. All compared packages must come from the same registry, and returned metadata fields differ slightly between npm and PyPI.", ), }, outputSchema: { packages: z.array(z.string()).describe( "Package names that were requested for comparison.", ), registry: z.enum(["npm", "pypi"]).describe( "Registry used for all comparisons.", ), comparisons: z.array(z.object({ name: z.string().describe( "Package name that was looked up.", ), found: z.boolean().describe( "True when the registry lookup succeeded and returned package metadata.", ), description: z.string().describe( "Short package summary from the registry.", ).optional(), latestVersion: z.string().describe( "Latest package version known to the registry.", ).optional(), license: z.union([z.string(), z.null()]).describe( "Package license metadata when provided by the registry.", ).optional(), lastPublished: z.union([z.string(), z.null()]).describe( "Publish timestamp of the latest version when npm provides one.", ).optional(), created: z.union([z.string(), z.null()]).describe( "Package creation timestamp when npm provides one.", ).optional(), totalVersions: z.number().int().nonnegative().describe( "Number of published versions when npm metadata includes a version history.", ).optional(), author: z.string().describe( "Package author when PyPI metadata includes one.", ).optional(), keywords: z.array(z.string()).describe( "Registry keywords or tags associated with the package.", ).optional(), cached: z.boolean().describe( "True when the lookup came from the 5-minute cache.", ).optional(), error: z.string().describe( "Fetch error when registry metadata could not be retrieved for this package.", ).optional(), })).describe( "Per-package lookup results returned in the same order as the input package list. Some fields only exist for npm or only for PyPI, so consumers should treat absent fields as normal.", ), }, annotations: readOnlyNetworkToolAnnotations,