registryx-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +203 -0
  2. package/coverage/lcov-report/base.css +224 -0
  3. package/coverage/lcov-report/block-navigation.js +87 -0
  4. package/coverage/lcov-report/favicon.png +0 -0
  5. package/coverage/lcov-report/index.html +146 -0
  6. package/coverage/lcov-report/prettify.css +1 -0
  7. package/coverage/lcov-report/prettify.js +2 -0
  8. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  9. package/coverage/lcov-report/sorter.js +210 -0
  10. package/coverage/lcov-report/src/adapters/crates.ts.html +847 -0
  11. package/coverage/lcov-report/src/adapters/index.html +176 -0
  12. package/coverage/lcov-report/src/adapters/index.ts.html +97 -0
  13. package/coverage/lcov-report/src/adapters/maven.ts.html +637 -0
  14. package/coverage/lcov-report/src/adapters/npm.ts.html +817 -0
  15. package/coverage/lcov-report/src/adapters/pypi.ts.html +730 -0
  16. package/coverage/lcov-report/src/cache.ts.html +202 -0
  17. package/coverage/lcov-report/src/config.ts.html +208 -0
  18. package/coverage/lcov-report/src/index.html +161 -0
  19. package/coverage/lcov-report/src/server.ts.html +1339 -0
  20. package/coverage/lcov-report/src/types.ts.html +373 -0
  21. package/coverage/lcov-report/src/utils/fetch.ts.html +220 -0
  22. package/coverage/lcov-report/src/utils/format.ts.html +130 -0
  23. package/coverage/lcov-report/src/utils/index.html +131 -0
  24. package/coverage/lcov.info +1686 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1210 -0
  27. package/eslint.config.mjs +16 -0
  28. package/package.json +41 -0
  29. package/src/adapters/crates.ts +254 -0
  30. package/src/adapters/index.ts +4 -0
  31. package/src/adapters/maven.ts +184 -0
  32. package/src/adapters/npm.ts +244 -0
  33. package/src/adapters/pypi.ts +215 -0
  34. package/src/cache.ts +39 -0
  35. package/src/config.ts +41 -0
  36. package/src/index.ts +25 -0
  37. package/src/server.ts +418 -0
  38. package/src/types.ts +96 -0
  39. package/src/utils/fetch.ts +45 -0
  40. package/src/utils/format.ts +15 -0
  41. package/test/adapters.test.ts +575 -0
  42. package/test/cache.test.ts +47 -0
  43. package/test/config.test.ts +69 -0
  44. package/test/fetch.test.ts +51 -0
  45. package/test/format.test.ts +35 -0
  46. package/test/server.test.ts +23 -0
  47. package/tsconfig.json +18 -0
  48. package/tsup.config.ts +11 -0
  49. package/vitest.config.ts +19 -0
package/dist/index.js ADDED
@@ -0,0 +1,1210 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+
4
+ // src/index.ts
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // src/config.ts
8
+ var VALID_REGISTRIES = /* @__PURE__ */ new Set(["npm", "pypi", "maven", "crates"]);
9
+ function loadConfig() {
10
+ const registriesEnv = process.env.REGISTRYX_MCP_REGISTRIES ?? "npm,pypi,maven,crates";
11
+ const registries = registriesEnv.split(",").map((r) => r.trim().toLowerCase()).filter((r) => VALID_REGISTRIES.has(r));
12
+ if (registries.length === 0) {
13
+ throw new Error(
14
+ "No valid registries configured. Set REGISTRYX_MCP_REGISTRIES to comma-separated values: npm, pypi, maven, crates"
15
+ );
16
+ }
17
+ const timeoutMs = parseInt(process.env.REGISTRYX_MCP_TIMEOUT_MS ?? "15000", 10);
18
+ if (isNaN(timeoutMs) || timeoutMs < 1e3 || timeoutMs > 12e4) {
19
+ throw new Error("REGISTRYX_MCP_TIMEOUT_MS must be between 1000 and 120000");
20
+ }
21
+ const cacheTtlMs = parseInt(process.env.REGISTRYX_MCP_CACHE_TTL_MS ?? "300000", 10);
22
+ if (isNaN(cacheTtlMs) || cacheTtlMs < 0) {
23
+ throw new Error("REGISTRYX_MCP_CACHE_TTL_MS must be >= 0");
24
+ }
25
+ return {
26
+ registries,
27
+ npmToken: process.env.REGISTRYX_MCP_NPM_TOKEN || void 0,
28
+ timeoutMs,
29
+ cacheTtlMs
30
+ };
31
+ }
32
+
33
+ // src/server.ts
34
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
35
+ import { z } from "zod";
36
+
37
+ // src/cache.ts
38
+ var Cache = class {
39
+ store = /* @__PURE__ */ new Map();
40
+ ttlMs;
41
+ constructor(ttlMs) {
42
+ this.ttlMs = ttlMs;
43
+ }
44
+ get(key) {
45
+ const entry = this.store.get(key);
46
+ if (!entry) return void 0;
47
+ if (Date.now() > entry.expiresAt) {
48
+ this.store.delete(key);
49
+ return void 0;
50
+ }
51
+ return entry.value;
52
+ }
53
+ set(key, value) {
54
+ if (this.ttlMs <= 0) return;
55
+ this.store.set(key, {
56
+ value,
57
+ expiresAt: Date.now() + this.ttlMs
58
+ });
59
+ }
60
+ clear() {
61
+ this.store.clear();
62
+ }
63
+ get size() {
64
+ return this.store.size;
65
+ }
66
+ };
67
+
68
+ // src/utils/fetch.ts
69
+ var ALLOWED_HOSTS = /* @__PURE__ */ new Set([
70
+ "registry.npmjs.org",
71
+ "api.npmjs.org",
72
+ "pypi.org",
73
+ "search.maven.org",
74
+ "crates.io",
75
+ "osv.dev"
76
+ ]);
77
+ async function registryFetch(url, config, extraHeaders) {
78
+ const parsed = new URL(url);
79
+ if (parsed.protocol !== "https:") {
80
+ throw new Error(`Only HTTPS URLs allowed, got: ${parsed.protocol}`);
81
+ }
82
+ if (!ALLOWED_HOSTS.has(parsed.hostname)) {
83
+ throw new Error(`Host not allowed: ${parsed.hostname}`);
84
+ }
85
+ const headers = {
86
+ Accept: "application/json",
87
+ "User-Agent": "registryx-mcp-server/0.1.0",
88
+ ...extraHeaders
89
+ };
90
+ if (parsed.hostname === "registry.npmjs.org" && config.npmToken) {
91
+ headers["Authorization"] = `Bearer ${config.npmToken}`;
92
+ }
93
+ const response = await fetch(url, {
94
+ headers,
95
+ signal: AbortSignal.timeout(config.timeoutMs)
96
+ });
97
+ if (!response.ok) {
98
+ throw new Error(`HTTP ${response.status} from ${parsed.hostname}${parsed.pathname}`);
99
+ }
100
+ return response;
101
+ }
102
+
103
+ // src/adapters/npm.ts
104
+ var NpmAdapter = class {
105
+ constructor(config, cache) {
106
+ this.config = config;
107
+ this.cache = cache;
108
+ }
109
+ config;
110
+ cache;
111
+ name = "npm";
112
+ async search(query, limit) {
113
+ const key = `npm:search:${query}:${limit}`;
114
+ const cached = this.cache.get(key);
115
+ if (cached) return cached;
116
+ const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`;
117
+ const res = await registryFetch(url, this.config);
118
+ const data = await res.json();
119
+ const results = (data.objects ?? []).map(
120
+ (obj) => ({
121
+ name: obj.package.name,
122
+ version: obj.package.version,
123
+ description: obj.package.description ?? "",
124
+ downloads: "",
125
+ registry: "npm"
126
+ })
127
+ );
128
+ this.cache.set(key, results);
129
+ return results;
130
+ }
131
+ async getPackage(name) {
132
+ const key = `npm:pkg:${name}`;
133
+ const cached = this.cache.get(key);
134
+ if (cached) return cached;
135
+ const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
136
+ const res = await registryFetch(url, this.config);
137
+ const data = await res.json();
138
+ const latest = data["dist-tags"]?.latest ?? "";
139
+ const result = {
140
+ name: data.name,
141
+ version: latest,
142
+ description: data.description ?? "",
143
+ license: typeof data.license === "string" ? data.license : data.license?.type ?? "Unknown",
144
+ homepage: data.homepage ?? "",
145
+ repository: typeof data.repository === "string" ? data.repository : data.repository?.url ?? "",
146
+ keywords: data.keywords ?? [],
147
+ registry: "npm"
148
+ };
149
+ this.cache.set(key, result);
150
+ return result;
151
+ }
152
+ async getReadme(name) {
153
+ const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
154
+ const res = await registryFetch(url, this.config);
155
+ const data = await res.json();
156
+ return data.readme ?? "No README available.";
157
+ }
158
+ async getMaintainers(name) {
159
+ const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
160
+ const res = await registryFetch(url, this.config);
161
+ const data = await res.json();
162
+ return (data.maintainers ?? []).map((m) => ({
163
+ name: m.name,
164
+ email: m.email
165
+ }));
166
+ }
167
+ async listVersions(name, limit, stableOnly) {
168
+ const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
169
+ const res = await registryFetch(url, this.config);
170
+ const data = await res.json();
171
+ const versions = Object.keys(data.versions ?? {}).reverse();
172
+ const timeMap = data.time ?? {};
173
+ let results = versions.map((v) => ({
174
+ version: v,
175
+ date: timeMap[v] ?? "",
176
+ prerelease: /[-+]/.test(v.replace(/^\d+\.\d+\.\d+/, ""))
177
+ }));
178
+ if (stableOnly) results = results.filter((v) => !v.prerelease);
179
+ return results.slice(0, limit);
180
+ }
181
+ async getVersion(name, version) {
182
+ const url = `https://registry.npmjs.org/${encodeURIComponent(name)}/${version}`;
183
+ const res = await registryFetch(url, this.config);
184
+ const data = await res.json();
185
+ const deps = Object.entries(data.dependencies ?? {}).map(([n, v]) => ({
186
+ name: n,
187
+ version: v,
188
+ type: "runtime"
189
+ }));
190
+ return {
191
+ name: data.name,
192
+ version: data.version,
193
+ date: "",
194
+ license: typeof data.license === "string" ? data.license : "Unknown",
195
+ size: data.dist?.unpackedSize ? `${Math.round(data.dist.unpackedSize / 1024)}KB` : "Unknown",
196
+ dependencies: deps,
197
+ registry: "npm"
198
+ };
199
+ }
200
+ async getDependencies(name, version, type) {
201
+ const url = `https://registry.npmjs.org/${encodeURIComponent(name)}/${version}`;
202
+ const res = await registryFetch(url, this.config);
203
+ const data = await res.json();
204
+ const deps = [];
205
+ if (type === "runtime" || type === "all") {
206
+ for (const [n, v] of Object.entries(data.dependencies ?? {})) {
207
+ deps.push({ name: n, version: v, type: "runtime" });
208
+ }
209
+ }
210
+ if (type === "dev" || type === "all") {
211
+ for (const [n, v] of Object.entries(data.devDependencies ?? {})) {
212
+ deps.push({ name: n, version: v, type: "dev" });
213
+ }
214
+ }
215
+ return deps;
216
+ }
217
+ async getReverseDependencies(name, limit) {
218
+ const url = `https://registry.npmjs.org/-/v1/search?text=dependencies:${encodeURIComponent(name)}&size=${limit}`;
219
+ const res = await registryFetch(url, this.config);
220
+ const data = await res.json();
221
+ return (data.objects ?? []).map(
222
+ (obj) => ({
223
+ name: obj.package.name,
224
+ version: obj.package.version,
225
+ description: obj.package.description ?? "",
226
+ downloads: "",
227
+ registry: "npm"
228
+ })
229
+ );
230
+ }
231
+ async getDownloadStats(name, period) {
232
+ const npmPeriod = period === "last-year" ? "last-year" : period;
233
+ const url = `https://api.npmjs.org/downloads/point/${npmPeriod}/${encodeURIComponent(name)}`;
234
+ const res = await registryFetch(url, this.config);
235
+ const data = await res.json();
236
+ return {
237
+ name,
238
+ registry: "npm",
239
+ period,
240
+ total: data.downloads ?? 0,
241
+ breakdown: []
242
+ };
243
+ }
244
+ async getPackageHealth(name) {
245
+ const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`;
246
+ const res = await registryFetch(url, this.config);
247
+ const data = await res.json();
248
+ const latest = data["dist-tags"]?.latest ?? "";
249
+ const latestInfo = data.versions?.[latest] ?? {};
250
+ const timeMap = data.time ?? {};
251
+ const lastPublish = timeMap[latest] ?? "";
252
+ const depCount = Object.keys(latestInfo.dependencies ?? {}).length;
253
+ const hasTypings = !!latestInfo.types || !!latestInfo.typings;
254
+ const signals = [];
255
+ if (hasTypings) signals.push("Has TypeScript typings");
256
+ if (latestInfo.scripts?.test) signals.push("Has test script");
257
+ if (data.readme && data.readme.length > 500) signals.push("Detailed README");
258
+ return {
259
+ name,
260
+ registry: "npm",
261
+ score: Math.min(10, signals.length * 3 + 4),
262
+ lastPublish,
263
+ dependencyCount: depCount,
264
+ hasTests: !!latestInfo.scripts?.test,
265
+ hasTypings,
266
+ signals
267
+ };
268
+ }
269
+ async getSecurityAdvisories(name, _version) {
270
+ try {
271
+ const url = "https://osv.dev/v1/query";
272
+ const body = { package: { name, ecosystem: "npm" } };
273
+ const res = await fetch(url, {
274
+ method: "POST",
275
+ headers: { "Content-Type": "application/json" },
276
+ body: JSON.stringify(body),
277
+ signal: AbortSignal.timeout(this.config.timeoutMs)
278
+ });
279
+ if (!res.ok) return [];
280
+ const data = await res.json();
281
+ return (data.vulns ?? []).map(
282
+ (v) => ({
283
+ id: v.id,
284
+ title: v.summary ?? v.id,
285
+ severity: v.severity?.[0]?.score ?? "Unknown",
286
+ url: v.references?.[0]?.url ?? `https://osv.dev/vulnerability/${v.id}`,
287
+ affectedVersions: "See advisory"
288
+ })
289
+ );
290
+ } catch {
291
+ return [];
292
+ }
293
+ }
294
+ };
295
+
296
+ // src/adapters/pypi.ts
297
+ var PypiAdapter = class {
298
+ constructor(config, cache) {
299
+ this.config = config;
300
+ this.cache = cache;
301
+ }
302
+ config;
303
+ cache;
304
+ name = "pypi";
305
+ async search(query, limit) {
306
+ const key = `pypi:search:${query}:${limit}`;
307
+ const cached = this.cache.get(key);
308
+ if (cached) return cached;
309
+ const results = [];
310
+ try {
311
+ const pkgUrl = `https://pypi.org/pypi/${encodeURIComponent(query)}/json`;
312
+ const res = await registryFetch(pkgUrl, this.config);
313
+ const data = await res.json();
314
+ results.push({
315
+ name: data.info.name,
316
+ version: data.info.version,
317
+ description: data.info.summary ?? "",
318
+ downloads: "",
319
+ registry: "pypi"
320
+ });
321
+ } catch {
322
+ }
323
+ this.cache.set(key, results.slice(0, limit));
324
+ return results.slice(0, limit);
325
+ }
326
+ async getPackage(name) {
327
+ const key = `pypi:pkg:${name}`;
328
+ const cached = this.cache.get(key);
329
+ if (cached) return cached;
330
+ const url = `https://pypi.org/pypi/${encodeURIComponent(name)}/json`;
331
+ const res = await registryFetch(url, this.config);
332
+ const data = await res.json();
333
+ const info = data.info;
334
+ const result = {
335
+ name: info.name,
336
+ version: info.version,
337
+ description: info.summary ?? "",
338
+ license: info.license ?? "Unknown",
339
+ homepage: info.home_page ?? info.project_url ?? "",
340
+ repository: info.project_urls?.Source ?? info.project_urls?.Repository ?? "",
341
+ keywords: info.keywords ? info.keywords.split(",").map((k) => k.trim()) : [],
342
+ registry: "pypi"
343
+ };
344
+ this.cache.set(key, result);
345
+ return result;
346
+ }
347
+ async getReadme(name) {
348
+ const url = `https://pypi.org/pypi/${encodeURIComponent(name)}/json`;
349
+ const res = await registryFetch(url, this.config);
350
+ const data = await res.json();
351
+ return data.info.description ?? "No README available.";
352
+ }
353
+ async getMaintainers(name) {
354
+ const url = `https://pypi.org/pypi/${encodeURIComponent(name)}/json`;
355
+ const res = await registryFetch(url, this.config);
356
+ const data = await res.json();
357
+ const info = data.info;
358
+ const maintainers = [];
359
+ if (info.author) maintainers.push({ name: info.author, email: info.author_email });
360
+ if (info.maintainer) maintainers.push({ name: info.maintainer, email: info.maintainer_email });
361
+ return maintainers;
362
+ }
363
+ async listVersions(name, limit, stableOnly) {
364
+ const url = `https://pypi.org/pypi/${encodeURIComponent(name)}/json`;
365
+ const res = await registryFetch(url, this.config);
366
+ const data = await res.json();
367
+ const releases = Object.keys(data.releases ?? {}).reverse();
368
+ let results = releases.map((v) => {
369
+ const files = data.releases[v] ?? [];
370
+ const date = files[0]?.upload_time_iso_8601 ?? "";
371
+ return {
372
+ version: v,
373
+ date,
374
+ prerelease: /(?:a|b|rc|dev|alpha|beta)\d*/i.test(v)
375
+ };
376
+ });
377
+ if (stableOnly) results = results.filter((v) => !v.prerelease);
378
+ return results.slice(0, limit);
379
+ }
380
+ async getVersion(name, version) {
381
+ const url = `https://pypi.org/pypi/${encodeURIComponent(name)}/${version}/json`;
382
+ const res = await registryFetch(url, this.config);
383
+ const data = await res.json();
384
+ const info = data.info;
385
+ const deps = (info.requires_dist ?? []).map((d) => {
386
+ const match = d.match(/^([^\s;]+)/);
387
+ return {
388
+ name: match?.[1] ?? d,
389
+ version: "",
390
+ type: "runtime"
391
+ };
392
+ });
393
+ return {
394
+ name: info.name,
395
+ version: info.version,
396
+ date: data.urls?.[0]?.upload_time_iso_8601 ?? "",
397
+ license: info.license ?? "Unknown",
398
+ size: data.urls?.[0]?.size ? `${Math.round(data.urls[0].size / 1024)}KB` : "Unknown",
399
+ dependencies: deps,
400
+ registry: "pypi"
401
+ };
402
+ }
403
+ async getDependencies(name, version, _type) {
404
+ const vd = await this.getVersion(name, version);
405
+ return vd.dependencies;
406
+ }
407
+ async getReverseDependencies(_name, _limit) {
408
+ return [];
409
+ }
410
+ async getDownloadStats(name, period) {
411
+ return {
412
+ name,
413
+ registry: "pypi",
414
+ period,
415
+ total: 0,
416
+ // PyPI doesn't have a public real-time download API
417
+ breakdown: []
418
+ };
419
+ }
420
+ async getPackageHealth(name) {
421
+ const url = `https://pypi.org/pypi/${encodeURIComponent(name)}/json`;
422
+ const res = await registryFetch(url, this.config);
423
+ const data = await res.json();
424
+ const info = data.info;
425
+ const signals = [];
426
+ if (info.description && info.description.length > 500) signals.push("Detailed description");
427
+ if (info.license) signals.push("License specified");
428
+ if (info.requires_python) signals.push("Python version specified");
429
+ if (info.project_urls?.Documentation) signals.push("Has documentation URL");
430
+ const depCount = (info.requires_dist ?? []).length;
431
+ return {
432
+ name,
433
+ registry: "pypi",
434
+ score: Math.min(10, signals.length * 2 + 3),
435
+ lastPublish: data.urls?.[0]?.upload_time_iso_8601 ?? "",
436
+ dependencyCount: depCount,
437
+ hasTests: false,
438
+ hasTypings: info.classifiers?.some((c) => c.includes("Typing")) ?? false,
439
+ signals
440
+ };
441
+ }
442
+ async getSecurityAdvisories(name, _version) {
443
+ try {
444
+ const url = "https://osv.dev/v1/query";
445
+ const body = { package: { name, ecosystem: "PyPI" } };
446
+ const res = await fetch(url, {
447
+ method: "POST",
448
+ headers: { "Content-Type": "application/json" },
449
+ body: JSON.stringify(body),
450
+ signal: AbortSignal.timeout(this.config.timeoutMs)
451
+ });
452
+ if (!res.ok) return [];
453
+ const data = await res.json();
454
+ return (data.vulns ?? []).map(
455
+ (v) => ({
456
+ id: v.id,
457
+ title: v.summary ?? v.id,
458
+ severity: v.severity?.[0]?.score ?? "Unknown",
459
+ url: v.references?.[0]?.url ?? `https://osv.dev/vulnerability/${v.id}`,
460
+ affectedVersions: "See advisory"
461
+ })
462
+ );
463
+ } catch {
464
+ return [];
465
+ }
466
+ }
467
+ };
468
+
469
+ // src/adapters/maven.ts
470
+ var MavenAdapter = class {
471
+ constructor(config, cache) {
472
+ this.config = config;
473
+ this.cache = cache;
474
+ }
475
+ config;
476
+ cache;
477
+ name = "maven";
478
+ parseCoords(name) {
479
+ const parts = name.split(":");
480
+ if (parts.length !== 2)
481
+ throw new Error(`Maven package must be groupId:artifactId, got: ${name}`);
482
+ return { groupId: parts[0], artifactId: parts[1] };
483
+ }
484
+ async search(query, limit) {
485
+ const key = `maven:search:${query}:${limit}`;
486
+ const cached = this.cache.get(key);
487
+ if (cached) return cached;
488
+ const url = `https://search.maven.org/solrsearch/select?q=${encodeURIComponent(query)}&rows=${limit}&wt=json`;
489
+ const res = await registryFetch(url, this.config);
490
+ const data = await res.json();
491
+ const results = (data.response?.docs ?? []).map(
492
+ (doc) => ({
493
+ name: `${doc.g}:${doc.a}`,
494
+ version: doc.latestVersion ?? "",
495
+ description: doc.p ?? "",
496
+ downloads: "",
497
+ registry: "maven"
498
+ })
499
+ );
500
+ this.cache.set(key, results);
501
+ return results;
502
+ }
503
+ async getPackage(name) {
504
+ const key = `maven:pkg:${name}`;
505
+ const cached = this.cache.get(key);
506
+ if (cached) return cached;
507
+ const { groupId, artifactId } = this.parseCoords(name);
508
+ const url = `https://search.maven.org/solrsearch/select?q=g:${encodeURIComponent(groupId)}+AND+a:${encodeURIComponent(artifactId)}&rows=1&wt=json`;
509
+ const res = await registryFetch(url, this.config);
510
+ const data = await res.json();
511
+ const doc = data.response?.docs?.[0];
512
+ if (!doc) throw new Error(`Package not found: ${name}`);
513
+ const result = {
514
+ name: `${doc.g}:${doc.a}`,
515
+ version: doc.latestVersion ?? "",
516
+ description: doc.p ?? "",
517
+ license: "",
518
+ homepage: "",
519
+ repository: "",
520
+ keywords: [],
521
+ registry: "maven"
522
+ };
523
+ this.cache.set(key, result);
524
+ return result;
525
+ }
526
+ async getReadme(_name) {
527
+ return "Maven Central does not provide README content via API.";
528
+ }
529
+ async getMaintainers(_name) {
530
+ return [];
531
+ }
532
+ async listVersions(name, limit, stableOnly) {
533
+ const { groupId, artifactId } = this.parseCoords(name);
534
+ const url = `https://search.maven.org/solrsearch/select?q=g:${encodeURIComponent(groupId)}+AND+a:${encodeURIComponent(artifactId)}&core=gav&rows=${limit * 2}&wt=json`;
535
+ const res = await registryFetch(url, this.config);
536
+ const data = await res.json();
537
+ let results = (data.response?.docs ?? []).map(
538
+ (doc) => ({
539
+ version: doc.v,
540
+ date: doc.timestamp ? new Date(doc.timestamp).toISOString() : "",
541
+ prerelease: /[-.](?:alpha|beta|rc|snapshot|milestone|m\d)/i.test(doc.v)
542
+ })
543
+ );
544
+ if (stableOnly) results = results.filter((v) => !v.prerelease);
545
+ return results.slice(0, limit);
546
+ }
547
+ async getVersion(name, version) {
548
+ const { groupId, artifactId } = this.parseCoords(name);
549
+ const url = `https://search.maven.org/solrsearch/select?q=g:${encodeURIComponent(groupId)}+AND+a:${encodeURIComponent(artifactId)}+AND+v:${encodeURIComponent(version)}&rows=1&wt=json`;
550
+ const res = await registryFetch(url, this.config);
551
+ const data = await res.json();
552
+ const doc = data.response?.docs?.[0];
553
+ if (!doc) throw new Error(`Version not found: ${name}@${version}`);
554
+ return {
555
+ name: `${doc.g}:${doc.a}`,
556
+ version: doc.v ?? version,
557
+ date: doc.timestamp ? new Date(doc.timestamp).toISOString() : "",
558
+ license: "",
559
+ size: "Unknown",
560
+ dependencies: [],
561
+ registry: "maven"
562
+ };
563
+ }
564
+ async getDependencies(_name, _version, _type) {
565
+ return [];
566
+ }
567
+ async getReverseDependencies(_name, _limit) {
568
+ return [];
569
+ }
570
+ async getDownloadStats(name, period) {
571
+ return { name, registry: "maven", period, total: 0, breakdown: [] };
572
+ }
573
+ async getPackageHealth(name) {
574
+ return {
575
+ name,
576
+ registry: "maven",
577
+ score: 5,
578
+ lastPublish: "",
579
+ dependencyCount: 0,
580
+ hasTests: false,
581
+ hasTypings: false,
582
+ signals: ["Available on Maven Central"]
583
+ };
584
+ }
585
+ async getSecurityAdvisories(name, _version) {
586
+ try {
587
+ const { groupId, artifactId } = this.parseCoords(name);
588
+ const url = "https://osv.dev/v1/query";
589
+ const body = { package: { name: `${groupId}:${artifactId}`, ecosystem: "Maven" } };
590
+ const res = await fetch(url, {
591
+ method: "POST",
592
+ headers: { "Content-Type": "application/json" },
593
+ body: JSON.stringify(body),
594
+ signal: AbortSignal.timeout(this.config.timeoutMs)
595
+ });
596
+ if (!res.ok) return [];
597
+ const data = await res.json();
598
+ return (data.vulns ?? []).map(
599
+ (v) => ({
600
+ id: v.id,
601
+ title: v.summary ?? v.id,
602
+ severity: v.severity?.[0]?.score ?? "Unknown",
603
+ url: v.references?.[0]?.url ?? `https://osv.dev/vulnerability/${v.id}`,
604
+ affectedVersions: "See advisory"
605
+ })
606
+ );
607
+ } catch {
608
+ return [];
609
+ }
610
+ }
611
+ };
612
+
613
+ // src/adapters/crates.ts
614
+ var CratesAdapter = class {
615
+ constructor(config, cache) {
616
+ this.config = config;
617
+ this.cache = cache;
618
+ }
619
+ config;
620
+ cache;
621
+ name = "crates";
622
+ async search(query, limit) {
623
+ const key = `crates:search:${query}:${limit}`;
624
+ const cached = this.cache.get(key);
625
+ if (cached) return cached;
626
+ const url = `https://crates.io/api/v1/crates?q=${encodeURIComponent(query)}&per_page=${limit}`;
627
+ const res = await registryFetch(url, this.config);
628
+ const data = await res.json();
629
+ const results = (data.crates ?? []).map(
630
+ (c) => ({
631
+ name: c.name,
632
+ version: c.max_version,
633
+ description: c.description ?? "",
634
+ downloads: `${c.downloads} total`,
635
+ registry: "crates"
636
+ })
637
+ );
638
+ this.cache.set(key, results);
639
+ return results;
640
+ }
641
+ async getPackage(name) {
642
+ const key = `crates:pkg:${name}`;
643
+ const cached = this.cache.get(key);
644
+ if (cached) return cached;
645
+ const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}`;
646
+ const res = await registryFetch(url, this.config);
647
+ const data = await res.json();
648
+ const crate = data.crate;
649
+ if (!crate) throw new Error(`Crate not found: ${name}`);
650
+ const result = {
651
+ name: crate.name,
652
+ version: crate.max_version ?? "",
653
+ description: crate.description ?? "",
654
+ license: "",
655
+ homepage: crate.homepage ?? "",
656
+ repository: crate.repository ?? "",
657
+ keywords: crate.keywords ?? [],
658
+ registry: "crates"
659
+ };
660
+ this.cache.set(key, result);
661
+ return result;
662
+ }
663
+ async getReadme(name) {
664
+ try {
665
+ const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}`;
666
+ const res = await registryFetch(url, this.config);
667
+ const data = await res.json();
668
+ return data.crate?.readme ?? `See https://crates.io/crates/${name}`;
669
+ } catch {
670
+ return "README not available.";
671
+ }
672
+ }
673
+ async getMaintainers(name) {
674
+ try {
675
+ const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/owner_user`;
676
+ const res = await registryFetch(url, this.config);
677
+ const data = await res.json();
678
+ return (data.users ?? []).map((u) => ({
679
+ name: u.name ?? u.login,
680
+ url: u.url
681
+ }));
682
+ } catch {
683
+ return [];
684
+ }
685
+ }
686
+ async listVersions(name, limit, stableOnly) {
687
+ const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/versions`;
688
+ const res = await registryFetch(url, this.config);
689
+ const data = await res.json();
690
+ let results = (data.versions ?? []).map(
691
+ (v) => ({
692
+ version: v.num,
693
+ date: v.created_at,
694
+ prerelease: /[-]/.test(v.num.replace(/^\d+\.\d+\.\d+/, ""))
695
+ })
696
+ );
697
+ if (stableOnly) results = results.filter((v) => !v.prerelease);
698
+ return results.slice(0, limit);
699
+ }
700
+ async getVersion(name, version) {
701
+ const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/${version}`;
702
+ const res = await registryFetch(url, this.config);
703
+ const data = await res.json();
704
+ const v = data.version;
705
+ if (!v) throw new Error(`Version not found: ${name}@${version}`);
706
+ const depUrl = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/${version}/dependencies`;
707
+ let deps = [];
708
+ try {
709
+ const depRes = await registryFetch(depUrl, this.config);
710
+ const depData = await depRes.json();
711
+ deps = (depData.dependencies ?? []).map(
712
+ (d) => ({
713
+ name: d.crate_id,
714
+ version: d.req,
715
+ type: d.kind === "dev" ? "dev" : "runtime"
716
+ })
717
+ );
718
+ } catch {
719
+ }
720
+ return {
721
+ name: v.crate ?? name,
722
+ version: v.num ?? version,
723
+ date: v.created_at ?? "",
724
+ license: v.license ?? "Unknown",
725
+ size: v.crate_size ? `${Math.round(v.crate_size / 1024)}KB` : "Unknown",
726
+ dependencies: deps,
727
+ registry: "crates"
728
+ };
729
+ }
730
+ async getDependencies(name, version, type) {
731
+ const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/${version}/dependencies`;
732
+ const res = await registryFetch(url, this.config);
733
+ const data = await res.json();
734
+ let deps = (data.dependencies ?? []).map(
735
+ (d) => ({
736
+ name: d.crate_id,
737
+ version: d.req,
738
+ type: d.kind === "dev" ? "dev" : "runtime"
739
+ })
740
+ );
741
+ if (type !== "all") {
742
+ deps = deps.filter((d) => d.type === type);
743
+ }
744
+ return deps;
745
+ }
746
+ async getReverseDependencies(name, limit) {
747
+ const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}/reverse_dependencies?per_page=${limit}`;
748
+ const res = await registryFetch(url, this.config);
749
+ const data = await res.json();
750
+ return (data.versions ?? []).map((v) => ({
751
+ name: v.crate ?? "",
752
+ version: v.num ?? "",
753
+ description: "",
754
+ downloads: "",
755
+ registry: "crates"
756
+ }));
757
+ }
758
+ async getDownloadStats(name, period) {
759
+ const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}`;
760
+ const res = await registryFetch(url, this.config);
761
+ const data = await res.json();
762
+ return {
763
+ name,
764
+ registry: "crates",
765
+ period,
766
+ total: data.crate?.downloads ?? 0,
767
+ breakdown: []
768
+ };
769
+ }
770
+ async getPackageHealth(name) {
771
+ const url = `https://crates.io/api/v1/crates/${encodeURIComponent(name)}`;
772
+ const res = await registryFetch(url, this.config);
773
+ const data = await res.json();
774
+ const crate = data.crate ?? {};
775
+ const signals = [];
776
+ if (crate.documentation) signals.push("Has documentation");
777
+ if (crate.repository) signals.push("Has repository");
778
+ if (crate.homepage) signals.push("Has homepage");
779
+ if (crate.description) signals.push("Has description");
780
+ return {
781
+ name,
782
+ registry: "crates",
783
+ score: Math.min(10, signals.length * 2 + 3),
784
+ lastPublish: crate.updated_at ?? "",
785
+ weeklyDownloads: crate.recent_downloads,
786
+ dependencyCount: 0,
787
+ hasTests: false,
788
+ hasTypings: false,
789
+ signals
790
+ };
791
+ }
792
+ async getSecurityAdvisories(name, _version) {
793
+ try {
794
+ const url = "https://osv.dev/v1/query";
795
+ const body = { package: { name, ecosystem: "crates.io" } };
796
+ const res = await fetch(url, {
797
+ method: "POST",
798
+ headers: { "Content-Type": "application/json" },
799
+ body: JSON.stringify(body),
800
+ signal: AbortSignal.timeout(this.config.timeoutMs)
801
+ });
802
+ if (!res.ok) return [];
803
+ const data = await res.json();
804
+ return (data.vulns ?? []).map(
805
+ (v) => ({
806
+ id: v.id,
807
+ title: v.summary ?? v.id,
808
+ severity: v.severity?.[0]?.score ?? "Unknown",
809
+ url: v.references?.[0]?.url ?? `https://osv.dev/vulnerability/${v.id}`,
810
+ affectedVersions: "See advisory"
811
+ })
812
+ );
813
+ } catch {
814
+ return [];
815
+ }
816
+ }
817
+ };
818
+
819
+ // src/utils/format.ts
820
+ function formatNumber(n) {
821
+ if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
822
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
823
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
824
+ return n.toString();
825
+ }
826
+ function padRight(str, len) {
827
+ return str.length >= len ? str : str + " ".repeat(len - str.length);
828
+ }
829
+ function truncate(str, maxLen) {
830
+ if (str.length <= maxLen) return str;
831
+ return str.slice(0, maxLen - 3) + "...";
832
+ }
833
+
834
+ // src/server.ts
835
+ function buildAdapters(config, cache) {
836
+ const map = /* @__PURE__ */ new Map();
837
+ const factories = {
838
+ npm: () => new NpmAdapter(config, cache),
839
+ pypi: () => new PypiAdapter(config, cache),
840
+ maven: () => new MavenAdapter(config, cache),
841
+ crates: () => new CratesAdapter(config, cache)
842
+ };
843
+ for (const reg of config.registries) {
844
+ map.set(reg, factories[reg]());
845
+ }
846
+ return map;
847
+ }
848
+ function getAdapter(adapters, name) {
849
+ const adapter = adapters.get(name);
850
+ if (!adapter)
851
+ throw new Error(
852
+ `Registry "${name}" is not enabled. Enabled: ${[...adapters.keys()].join(", ")}`
853
+ );
854
+ return adapter;
855
+ }
856
+ function getAdaptersForRegistry(adapters, registry) {
857
+ if (registry === "all") return [...adapters.values()];
858
+ return [getAdapter(adapters, registry)];
859
+ }
860
+ var registryEnum = z.enum(["npm", "pypi", "maven", "crates"]);
861
+ var registryOrAll = z.enum(["npm", "pypi", "maven", "crates", "all"]).default("all");
862
+ function createServer(config) {
863
+ const cache = new Cache(config.cacheTtlMs);
864
+ const adapters = buildAdapters(config, cache);
865
+ const server = new McpServer({
866
+ name: "registryx",
867
+ version: "0.1.0"
868
+ });
869
+ server.tool(
870
+ "registryx_search",
871
+ "Search packages across npm, PyPI, Maven Central, and crates.io registries",
872
+ {
873
+ query: z.string().describe("Package search text"),
874
+ registry: registryOrAll.describe("Registry to search (default: all)"),
875
+ limit: z.number().min(1).max(50).default(10).describe("Max results per registry")
876
+ },
877
+ async ({ query, registry, limit }) => {
878
+ const targets = getAdaptersForRegistry(adapters, registry);
879
+ const sections = [`RegistryX Search \u2014 "${query}"
880
+ `];
881
+ const results = await Promise.allSettled(
882
+ targets.map(async (a) => ({ name: a.name, results: await a.search(query, limit) }))
883
+ );
884
+ for (const r of results) {
885
+ if (r.status === "fulfilled") {
886
+ const { name, results: pkgs } = r.value;
887
+ sections.push(`\u{1F4E6} ${name} (${pkgs.length} results):`);
888
+ for (const p of pkgs) {
889
+ sections.push(
890
+ ` ${padRight(p.name, 30)} ${padRight(p.version, 12)} ${p.downloads ? padRight(p.downloads, 14) : ""}${truncate(p.description, 60)}`
891
+ );
892
+ }
893
+ sections.push("");
894
+ } else {
895
+ sections.push(`\u274C Error: ${r.reason}`);
896
+ }
897
+ }
898
+ return { content: [{ type: "text", text: sections.join("\n") }] };
899
+ }
900
+ );
901
+ server.tool(
902
+ "registryx_search_alternatives",
903
+ "Find equivalent packages across different registries",
904
+ {
905
+ packageName: z.string().describe("Package name to find alternatives for"),
906
+ sourceRegistry: registryEnum.describe("Registry the package is from"),
907
+ targetRegistries: z.array(registryEnum).optional().describe("Registries to search in")
908
+ },
909
+ async ({ packageName, sourceRegistry, targetRegistries }) => {
910
+ const source = getAdapter(adapters, sourceRegistry);
911
+ const pkg = await source.getPackage(packageName);
912
+ const searchTerms = [pkg.name.replace(/[^a-zA-Z0-9 ]/g, " "), ...pkg.keywords.slice(0, 3)];
913
+ const query = searchTerms.join(" ");
914
+ const targets = targetRegistries ? targetRegistries.filter((r) => r !== sourceRegistry).map((r) => getAdapter(adapters, r)) : [...adapters.values()].filter((a) => a.name !== sourceRegistry);
915
+ const sections = [
916
+ `Alternatives for "${packageName}" (${sourceRegistry}): ${pkg.description}
917
+ `
918
+ ];
919
+ const results = await Promise.allSettled(
920
+ targets.map(async (a) => ({ name: a.name, results: await a.search(query, 5) }))
921
+ );
922
+ for (const r of results) {
923
+ if (r.status === "fulfilled") {
924
+ const { name, results: pkgs } = r.value;
925
+ sections.push(`\u{1F4E6} ${name}:`);
926
+ for (const p of pkgs) {
927
+ sections.push(
928
+ ` ${padRight(p.name, 30)} ${padRight(p.version, 12)} ${truncate(p.description, 60)}`
929
+ );
930
+ }
931
+ sections.push("");
932
+ }
933
+ }
934
+ return { content: [{ type: "text", text: sections.join("\n") }] };
935
+ }
936
+ );
937
+ server.tool(
938
+ "registryx_get_package",
939
+ "Get detailed package metadata from a registry",
940
+ {
941
+ name: z.string().describe("Package name (Maven: groupId:artifactId)"),
942
+ registry: registryEnum.describe("Package registry")
943
+ },
944
+ async ({ name, registry }) => {
945
+ const adapter = getAdapter(adapters, registry);
946
+ const pkg = await adapter.getPackage(name);
947
+ const lines = [
948
+ `\u{1F4E6} ${pkg.name} v${pkg.version} (${pkg.registry})`,
949
+ `Description: ${pkg.description}`,
950
+ `License: ${pkg.license}`,
951
+ `Homepage: ${pkg.homepage || "N/A"}`,
952
+ `Repository: ${pkg.repository || "N/A"}`,
953
+ `Keywords: ${pkg.keywords.join(", ") || "N/A"}`
954
+ ];
955
+ return { content: [{ type: "text", text: lines.join("\n") }] };
956
+ }
957
+ );
958
+ server.tool(
959
+ "registryx_get_readme",
960
+ "Get README content for a package",
961
+ {
962
+ name: z.string().describe("Package name"),
963
+ registry: registryEnum.describe("Package registry")
964
+ },
965
+ async ({ name, registry }) => {
966
+ const adapter = getAdapter(adapters, registry);
967
+ const readme = await adapter.getReadme(name);
968
+ return { content: [{ type: "text", text: truncate(readme, 8e3) }] };
969
+ }
970
+ );
971
+ server.tool(
972
+ "registryx_get_maintainers",
973
+ "Get package maintainers/authors",
974
+ {
975
+ name: z.string().describe("Package name"),
976
+ registry: registryEnum.describe("Package registry")
977
+ },
978
+ async ({ name, registry }) => {
979
+ const adapter = getAdapter(adapters, registry);
980
+ const maintainers = await adapter.getMaintainers(name);
981
+ if (maintainers.length === 0)
982
+ return { content: [{ type: "text", text: "No maintainer info available." }] };
983
+ const lines = [`Maintainers for ${name} (${registry}):`];
984
+ for (const m of maintainers) {
985
+ lines.push(` \u{1F464} ${m.name}${m.email ? ` <${m.email}>` : ""}${m.url ? ` (${m.url})` : ""}`);
986
+ }
987
+ return { content: [{ type: "text", text: lines.join("\n") }] };
988
+ }
989
+ );
990
+ server.tool(
991
+ "registryx_list_versions",
992
+ "List available versions for a package",
993
+ {
994
+ name: z.string().describe("Package name"),
995
+ registry: registryEnum.describe("Package registry"),
996
+ limit: z.number().min(1).max(100).default(20).describe("Max versions to return"),
997
+ stable: z.boolean().default(true).describe("Exclude pre-release versions")
998
+ },
999
+ async ({ name, registry, limit, stable }) => {
1000
+ const adapter = getAdapter(adapters, registry);
1001
+ const versions = await adapter.listVersions(name, limit, stable);
1002
+ const lines = [`Versions for ${name} (${registry}) \u2014 ${stable ? "stable only" : "all"}:`];
1003
+ for (const v of versions) {
1004
+ lines.push(
1005
+ ` ${padRight(v.version, 20)} ${v.date ? v.date.split("T")[0] : ""}${v.prerelease ? " [pre]" : ""}`
1006
+ );
1007
+ }
1008
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1009
+ }
1010
+ );
1011
+ server.tool(
1012
+ "registryx_get_version",
1013
+ "Get details for a specific package version",
1014
+ {
1015
+ name: z.string().describe("Package name"),
1016
+ registry: registryEnum.describe("Package registry"),
1017
+ version: z.string().describe("Version string")
1018
+ },
1019
+ async ({ name, registry, version }) => {
1020
+ const adapter = getAdapter(adapters, registry);
1021
+ const vd = await adapter.getVersion(name, version);
1022
+ const lines = [
1023
+ `\u{1F4E6} ${vd.name} v${vd.version} (${vd.registry})`,
1024
+ `Date: ${vd.date || "Unknown"}`,
1025
+ `License: ${vd.license}`,
1026
+ `Size: ${vd.size}`,
1027
+ `Dependencies: ${vd.dependencies.length}`
1028
+ ];
1029
+ if (vd.dependencies.length > 0) {
1030
+ lines.push("");
1031
+ for (const d of vd.dependencies.slice(0, 20)) {
1032
+ lines.push(` ${d.type === "dev" ? "[dev] " : ""}${d.name} ${d.version}`);
1033
+ }
1034
+ }
1035
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1036
+ }
1037
+ );
1038
+ server.tool(
1039
+ "registryx_compare_versions",
1040
+ "Compare two versions of a package",
1041
+ {
1042
+ name: z.string().describe("Package name"),
1043
+ registry: registryEnum.describe("Package registry"),
1044
+ version1: z.string().describe("First version"),
1045
+ version2: z.string().describe("Second version")
1046
+ },
1047
+ async ({ name, registry, version1, version2 }) => {
1048
+ const adapter = getAdapter(adapters, registry);
1049
+ const [v1, v2] = await Promise.all([
1050
+ adapter.getVersion(name, version1),
1051
+ adapter.getVersion(name, version2)
1052
+ ]);
1053
+ const deps1 = new Set(v1.dependencies.map((d) => d.name));
1054
+ const deps2 = new Set(v2.dependencies.map((d) => d.name));
1055
+ const added = [...deps2].filter((d) => !deps1.has(d));
1056
+ const removed = [...deps1].filter((d) => !deps2.has(d));
1057
+ const lines = [
1058
+ `Comparing ${name} v${version1} \u2194 v${version2} (${registry})`,
1059
+ "",
1060
+ ` v${version1}: ${v1.date || "Unknown"} | ${v1.license} | ${v1.size} | ${v1.dependencies.length} deps`,
1061
+ ` v${version2}: ${v2.date || "Unknown"} | ${v2.license} | ${v2.size} | ${v2.dependencies.length} deps`
1062
+ ];
1063
+ if (added.length > 0) lines.push(`
1064
+ \u2795 Added: ${added.join(", ")}`);
1065
+ if (removed.length > 0) lines.push(` \u2796 Removed: ${removed.join(", ")}`);
1066
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1067
+ }
1068
+ );
1069
+ server.tool(
1070
+ "registryx_get_dependencies",
1071
+ "Get package dependencies",
1072
+ {
1073
+ name: z.string().describe("Package name"),
1074
+ registry: registryEnum.describe("Package registry"),
1075
+ version: z.string().optional().describe("Version (latest if omitted)"),
1076
+ type: z.enum(["runtime", "dev", "all"]).default("runtime").describe("Dependency type")
1077
+ },
1078
+ async ({ name, registry, version, type }) => {
1079
+ const adapter = getAdapter(adapters, registry);
1080
+ const ver = version ?? (await adapter.getPackage(name)).version;
1081
+ const deps = await adapter.getDependencies(name, ver, type);
1082
+ if (deps.length === 0)
1083
+ return {
1084
+ content: [{ type: "text", text: `No ${type} dependencies found for ${name}@${ver}.` }]
1085
+ };
1086
+ const lines = [`Dependencies for ${name}@${ver} (${registry}, ${type}):`];
1087
+ for (const d of deps) {
1088
+ lines.push(` ${d.type === "dev" ? "[dev] " : ""}${padRight(d.name, 30)} ${d.version}`);
1089
+ }
1090
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1091
+ }
1092
+ );
1093
+ server.tool(
1094
+ "registryx_reverse_dependencies",
1095
+ "Find packages that depend on this one",
1096
+ {
1097
+ name: z.string().describe("Package name"),
1098
+ registry: registryEnum.describe("Package registry"),
1099
+ limit: z.number().min(1).max(100).default(20).describe("Max results")
1100
+ },
1101
+ async ({ name, registry, limit }) => {
1102
+ const adapter = getAdapter(adapters, registry);
1103
+ const rdeps = await adapter.getReverseDependencies(name, limit);
1104
+ if (rdeps.length === 0)
1105
+ return {
1106
+ content: [
1107
+ {
1108
+ type: "text",
1109
+ text: `No reverse dependency data available for ${name} on ${registry}.`
1110
+ }
1111
+ ]
1112
+ };
1113
+ const lines = [`Packages that depend on ${name} (${registry}):`];
1114
+ for (const p of rdeps) {
1115
+ lines.push(` ${padRight(p.name, 30)} ${p.version}`);
1116
+ }
1117
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1118
+ }
1119
+ );
1120
+ server.tool(
1121
+ "registryx_download_stats",
1122
+ "Get download statistics for a package",
1123
+ {
1124
+ name: z.string().describe("Package name"),
1125
+ registry: registryEnum.describe("Package registry"),
1126
+ period: z.enum(["last-day", "last-week", "last-month", "last-year"]).default("last-month").describe("Time period")
1127
+ },
1128
+ async ({ name, registry, period }) => {
1129
+ const adapter = getAdapter(adapters, registry);
1130
+ const stats = await adapter.getDownloadStats(name, period);
1131
+ const lines = [
1132
+ `\u{1F4CA} Download stats for ${name} (${registry})`,
1133
+ `Period: ${stats.period}`,
1134
+ `Total: ${formatNumber(stats.total)}`
1135
+ ];
1136
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1137
+ }
1138
+ );
1139
+ server.tool(
1140
+ "registryx_package_health",
1141
+ "Get package health score and maintenance signals",
1142
+ {
1143
+ name: z.string().describe("Package name"),
1144
+ registry: registryEnum.describe("Package registry")
1145
+ },
1146
+ async ({ name, registry }) => {
1147
+ const adapter = getAdapter(adapters, registry);
1148
+ const health = await adapter.getPackageHealth(name);
1149
+ const lines = [
1150
+ `\u{1F3E5} Health for ${name} (${registry})`,
1151
+ `Score: ${health.score}/10`,
1152
+ `Last publish: ${health.lastPublish || "Unknown"}`,
1153
+ `Dependencies: ${health.dependencyCount}`,
1154
+ `Has typings: ${health.hasTypings ? "\u2705" : "\u274C"}`,
1155
+ `Has tests: ${health.hasTests ? "\u2705" : "\u274C"}`,
1156
+ "",
1157
+ "Signals:",
1158
+ ...health.signals.map((s) => ` \u2705 ${s}`)
1159
+ ];
1160
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1161
+ }
1162
+ );
1163
+ server.tool(
1164
+ "registryx_security_advisories",
1165
+ "Check for security advisories (via OSV.dev)",
1166
+ {
1167
+ name: z.string().describe("Package name"),
1168
+ registry: registryEnum.describe("Package registry"),
1169
+ version: z.string().optional().describe("Specific version to check")
1170
+ },
1171
+ async ({ name, registry, version }) => {
1172
+ const adapter = getAdapter(adapters, registry);
1173
+ const advisories = await adapter.getSecurityAdvisories(name, version);
1174
+ if (advisories.length === 0) {
1175
+ return {
1176
+ content: [
1177
+ { type: "text", text: `\u2705 No known security advisories for ${name} (${registry}).` }
1178
+ ]
1179
+ };
1180
+ }
1181
+ const lines = [`\u{1F512} Security advisories for ${name} (${registry}):`];
1182
+ for (const a of advisories) {
1183
+ lines.push(` \u26A0\uFE0F ${a.id}: ${a.title}`);
1184
+ lines.push(` Severity: ${a.severity} | ${a.url}`);
1185
+ }
1186
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1187
+ }
1188
+ );
1189
+ return server;
1190
+ }
1191
+
1192
+ // src/index.ts
1193
+ async function main() {
1194
+ const config = loadConfig();
1195
+ const server = createServer(config);
1196
+ const transport = new StdioServerTransport();
1197
+ await server.connect(transport);
1198
+ process.on("SIGINT", async () => {
1199
+ await server.close();
1200
+ process.exit(0);
1201
+ });
1202
+ process.on("SIGTERM", async () => {
1203
+ await server.close();
1204
+ process.exit(0);
1205
+ });
1206
+ }
1207
+ main().catch((err) => {
1208
+ console.error("Fatal:", err);
1209
+ process.exit(1);
1210
+ });