loadout-ai 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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/MASTER_PLAN.md +1711 -0
  3. package/README.md +453 -0
  4. package/SECURITY.md +18 -0
  5. package/SIMPLE_PLAN.md +44 -0
  6. package/catalog/discovered.json +10469 -0
  7. package/catalog/packages.json +1150 -0
  8. package/dashboard/app.js +607 -0
  9. package/dashboard/index.html +249 -0
  10. package/dashboard/styles.css +384 -0
  11. package/dist/src/cli.js +2735 -0
  12. package/dist/src/core/active-policy.js +233 -0
  13. package/dist/src/core/active-set.js +382 -0
  14. package/dist/src/core/adapters.js +254 -0
  15. package/dist/src/core/adopt.js +92 -0
  16. package/dist/src/core/agent-health-score.js +475 -0
  17. package/dist/src/core/agent-inspection.js +152 -0
  18. package/dist/src/core/agent-versions.js +131 -0
  19. package/dist/src/core/api.js +90 -0
  20. package/dist/src/core/atomic-file.js +22 -0
  21. package/dist/src/core/audit.js +174 -0
  22. package/dist/src/core/benchmark-campaign.js +496 -0
  23. package/dist/src/core/benchmark-evidence.js +458 -0
  24. package/dist/src/core/benchmark-fixtures.js +1157 -0
  25. package/dist/src/core/benchmark-runner.js +301 -0
  26. package/dist/src/core/benchmark-trust.js +795 -0
  27. package/dist/src/core/canary.js +77 -0
  28. package/dist/src/core/candidate-intelligence.js +661 -0
  29. package/dist/src/core/catalog-coverage.js +85 -0
  30. package/dist/src/core/catalog-install.js +186 -0
  31. package/dist/src/core/catalog-release.js +286 -0
  32. package/dist/src/core/catalog.js +327 -0
  33. package/dist/src/core/codex-mcp.js +122 -0
  34. package/dist/src/core/community.js +86 -0
  35. package/dist/src/core/compatibility-intelligence.js +465 -0
  36. package/dist/src/core/completion.js +160 -0
  37. package/dist/src/core/components.js +138 -0
  38. package/dist/src/core/conversion.js +56 -0
  39. package/dist/src/core/credentials.js +377 -0
  40. package/dist/src/core/demo.js +136 -0
  41. package/dist/src/core/diff.js +83 -0
  42. package/dist/src/core/discovery-connector.js +165 -0
  43. package/dist/src/core/doctor.js +95 -0
  44. package/dist/src/core/ecosystem-import.js +1072 -0
  45. package/dist/src/core/evaluate.js +72 -0
  46. package/dist/src/core/file-lock.js +140 -0
  47. package/dist/src/core/freshness-alerts.js +230 -0
  48. package/dist/src/core/github-discovery.js +97 -0
  49. package/dist/src/core/github.js +148 -0
  50. package/dist/src/core/head-to-head.js +437 -0
  51. package/dist/src/core/health-score-evidence.js +169 -0
  52. package/dist/src/core/health.js +129 -0
  53. package/dist/src/core/improve.js +223 -0
  54. package/dist/src/core/install.js +233 -0
  55. package/dist/src/core/intelligence-feed-build.js +59 -0
  56. package/dist/src/core/intelligence-feed.js +353 -0
  57. package/dist/src/core/loadout-badge.js +55 -0
  58. package/dist/src/core/loadout-card.js +87 -0
  59. package/dist/src/core/manifest.js +307 -0
  60. package/dist/src/core/mcp-recipes.js +419 -0
  61. package/dist/src/core/mcp-registry-discovery.js +455 -0
  62. package/dist/src/core/mcp.js +287 -0
  63. package/dist/src/core/model-config.js +102 -0
  64. package/dist/src/core/observations.js +87 -0
  65. package/dist/src/core/outcomes.js +164 -0
  66. package/dist/src/core/package.js +225 -0
  67. package/dist/src/core/paths.js +214 -0
  68. package/dist/src/core/portable.js +130 -0
  69. package/dist/src/core/private-discovery.js +45 -0
  70. package/dist/src/core/profiles.js +255 -0
  71. package/dist/src/core/provenance.js +278 -0
  72. package/dist/src/core/ranking.js +104 -0
  73. package/dist/src/core/recommend.js +181 -0
  74. package/dist/src/core/registry-api.js +97 -0
  75. package/dist/src/core/registry.js +316 -0
  76. package/dist/src/core/release-claims.js +175 -0
  77. package/dist/src/core/remove.js +122 -0
  78. package/dist/src/core/review-queue.js +200 -0
  79. package/dist/src/core/runtime-tool-recipe.js +315 -0
  80. package/dist/src/core/runtime-tools.js +453 -0
  81. package/dist/src/core/safety.js +234 -0
  82. package/dist/src/core/sandbox.js +78 -0
  83. package/dist/src/core/scheduler.js +264 -0
  84. package/dist/src/core/share-report.js +85 -0
  85. package/dist/src/core/signing.js +115 -0
  86. package/dist/src/core/skill-compare.js +239 -0
  87. package/dist/src/core/skill-inventory.js +193 -0
  88. package/dist/src/core/skill-security.js +340 -0
  89. package/dist/src/core/skills-sh-discovery.js +336 -0
  90. package/dist/src/core/skills.js +208 -0
  91. package/dist/src/core/snapshot.js +210 -0
  92. package/dist/src/core/source.js +305 -0
  93. package/dist/src/core/state.js +300 -0
  94. package/dist/src/core/sync.js +348 -0
  95. package/dist/src/core/terminal.js +53 -0
  96. package/dist/src/core/transaction.js +211 -0
  97. package/dist/src/core/update-watch.js +39 -0
  98. package/dist/src/core/update.js +216 -0
  99. package/dist/src/core/upgrade.js +207 -0
  100. package/dist/src/dashboard.js +418 -0
  101. package/dist/src/shared/schemas.js +336 -0
  102. package/dist/src/shared/types.js +1 -0
  103. package/docs/ACTIVE_SET.md +53 -0
  104. package/docs/ACTIVE_SET_POLICY.md +45 -0
  105. package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
  106. package/docs/CATALOG.md +78 -0
  107. package/docs/CATALOG_POLICY.md +41 -0
  108. package/docs/COMMUNITY_DISCOVERY.md +14 -0
  109. package/docs/COMPATIBILITY_POLICY.md +22 -0
  110. package/docs/CONVERSION_AND_SANDBOX.md +27 -0
  111. package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
  112. package/docs/DISCOVERED.md +280 -0
  113. package/docs/EVALUATION_PROTOCOL.md +40 -0
  114. package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
  115. package/docs/FEATURE_TEST_MATRIX.md +802 -0
  116. package/docs/GITHUB_AUTHORIZATION.md +38 -0
  117. package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
  118. package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
  119. package/docs/PROVIDER_CONFIGURATION.md +45 -0
  120. package/docs/RELEASE_REVIEW.md +117 -0
  121. package/docs/SAFE_UPDATE_DEMO.md +25 -0
  122. package/docs/SCHEMA_DECISIONS.md +25 -0
  123. package/docs/TEAM_POLICY.md +18 -0
  124. package/docs/TESTING.md +190 -0
  125. package/package.json +82 -0
@@ -0,0 +1,455 @@
1
+ import { join } from "node:path";
2
+ import { ConnectorRequestError, fetchBoundedJson, rateLimitFromHeaders, readDiscoveryConnectorCache, writeDiscoveryConnectorCache, } from "./discovery-connector.js";
3
+ import { loadoutHome } from "./paths.js";
4
+ const SOURCE = "official-mcp-registry";
5
+ const API = "https://registry.modelcontextprotocol.io/v0.1/servers";
6
+ const ATTRIBUTION = "https://registry.modelcontextprotocol.io/docs";
7
+ const SERVER_NAME = /^[A-Za-z0-9.-]+\/[A-Za-z0-9._-]+$/;
8
+ function isRecord(value) {
9
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
10
+ }
11
+ function boundedString(value, maximum, options = {}) {
12
+ return (typeof value === "string" &&
13
+ value.length <= maximum &&
14
+ (options.empty || value.length > 0) &&
15
+ !value.includes("\0") &&
16
+ (options.multiline || !/[\r\n]/.test(value)));
17
+ }
18
+ function validDate(value) {
19
+ return (boundedString(value, 64) && Number.isFinite(Date.parse(value)));
20
+ }
21
+ function safeRelativePath(value) {
22
+ return (boundedString(value, 1_024) &&
23
+ !value.startsWith("/") &&
24
+ !value.split(/[\\/]/).includes(".."));
25
+ }
26
+ function safeHttpsUrl(value) {
27
+ if (!boundedString(value, 2_048))
28
+ return false;
29
+ try {
30
+ const url = new URL(value);
31
+ return url.protocol === "https:" && !url.username && !url.password;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ function githubRepository(value) {
38
+ try {
39
+ const url = new URL(value);
40
+ if (url.origin !== "https://github.com")
41
+ return undefined;
42
+ const path = url.pathname
43
+ .replace(/^\//, "")
44
+ .replace(/\.git$/i, "")
45
+ .replace(/\/$/, "");
46
+ return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(path) ? path : undefined;
47
+ }
48
+ catch {
49
+ return undefined;
50
+ }
51
+ }
52
+ function transportType(value) {
53
+ if (!isRecord(value))
54
+ return undefined;
55
+ return value.type === "stdio" ||
56
+ value.type === "sse" ||
57
+ value.type === "streamable-http"
58
+ ? value.type
59
+ : undefined;
60
+ }
61
+ function normalizePackages(value) {
62
+ if (value === undefined)
63
+ return [];
64
+ if (!Array.isArray(value) || value.length > 50)
65
+ return undefined;
66
+ const distributions = [];
67
+ for (const item of value) {
68
+ if (!isRecord(item))
69
+ return undefined;
70
+ const transport = transportType(item.transport);
71
+ if (!boundedString(item.registryType, 64) ||
72
+ !boundedString(item.identifier, 2_048) ||
73
+ !transport ||
74
+ (item.version !== undefined && !boundedString(item.version, 255)) ||
75
+ (item.fileSha256 !== undefined &&
76
+ (typeof item.fileSha256 !== "string" ||
77
+ !/^[a-f0-9]{64}$/.test(item.fileSha256))))
78
+ return undefined;
79
+ distributions.push({
80
+ kind: "package",
81
+ type: item.registryType,
82
+ identifier: item.identifier,
83
+ ...(typeof item.version === "string" ? { version: item.version } : {}),
84
+ transport,
85
+ ...(typeof item.fileSha256 === "string"
86
+ ? { integritySha256: item.fileSha256 }
87
+ : {}),
88
+ });
89
+ }
90
+ return distributions;
91
+ }
92
+ function normalizeRemotes(value) {
93
+ if (value === undefined)
94
+ return [];
95
+ if (!Array.isArray(value) || value.length > 50)
96
+ return undefined;
97
+ const distributions = [];
98
+ for (const item of value) {
99
+ if (!isRecord(item))
100
+ return undefined;
101
+ const transport = transportType(item);
102
+ if ((transport !== "sse" && transport !== "streamable-http") ||
103
+ !safeHttpsUrl(item.url))
104
+ return undefined;
105
+ distributions.push({
106
+ kind: "remote",
107
+ type: transport,
108
+ identifier: item.url,
109
+ transport,
110
+ });
111
+ }
112
+ return distributions;
113
+ }
114
+ function normalizeRepository(value) {
115
+ if (value === undefined)
116
+ return undefined;
117
+ if (!isRecord(value) ||
118
+ !safeHttpsUrl(value.url) ||
119
+ !boundedString(value.source, 100) ||
120
+ (value.id !== undefined && !boundedString(value.id, 255)) ||
121
+ (value.subfolder !== undefined && !safeRelativePath(value.subfolder)))
122
+ return false;
123
+ const repository = value.source.toLowerCase() === "github"
124
+ ? githubRepository(value.url)
125
+ : undefined;
126
+ if (value.source.toLowerCase() === "github" && !repository)
127
+ return false;
128
+ return {
129
+ url: value.url,
130
+ source: value.source,
131
+ ...(typeof value.id === "string" ? { id: value.id } : {}),
132
+ ...(typeof value.subfolder === "string"
133
+ ? { subfolder: value.subfolder }
134
+ : {}),
135
+ ...(repository ? { repository } : {}),
136
+ };
137
+ }
138
+ function officialMetadata(value) {
139
+ if (!isRecord(value))
140
+ return undefined;
141
+ const official = value["io.modelcontextprotocol.registry/official"];
142
+ return isRecord(official) ? official : undefined;
143
+ }
144
+ function normalizeServer(value, observedAt) {
145
+ if (!isRecord(value) || !isRecord(value.server))
146
+ return undefined;
147
+ const server = value.server;
148
+ if (!boundedString(server.name, 200) ||
149
+ !SERVER_NAME.test(server.name) ||
150
+ !boundedString(server.description, 100, { multiline: true }) ||
151
+ !boundedString(server.version, 255) ||
152
+ server.version === "latest" ||
153
+ (server.title !== undefined && !boundedString(server.title, 100)))
154
+ return undefined;
155
+ const repository = normalizeRepository(server.repository);
156
+ const packages = normalizePackages(server.packages);
157
+ const remotes = normalizeRemotes(server.remotes);
158
+ if (repository === false || !packages || !remotes)
159
+ return undefined;
160
+ const meta = officialMetadata(value._meta);
161
+ const status = meta?.status;
162
+ const lifecycleStatus = status === "active" || status === "deprecated" || status === "deleted"
163
+ ? status
164
+ : "unknown";
165
+ if ((meta?.status !== undefined && lifecycleStatus === "unknown") ||
166
+ (meta?.statusMessage !== undefined &&
167
+ !boundedString(meta.statusMessage, 500, { multiline: true })) ||
168
+ (meta?.publishedAt !== undefined && !validDate(meta.publishedAt)) ||
169
+ (meta?.updatedAt !== undefined && !validDate(meta.updatedAt)) ||
170
+ (meta?.statusChangedAt !== undefined && !validDate(meta.statusChangedAt)) ||
171
+ (meta?.isLatest !== undefined && typeof meta.isLatest !== "boolean"))
172
+ return undefined;
173
+ const namespace = server.name.split("/", 1)[0];
174
+ const repositoryKey = repository?.repository
175
+ ? `github:${repository.repository.toLowerCase()}`
176
+ : undefined;
177
+ const sourceUrl = `${API}/${encodeURIComponent(server.name)}/versions/${encodeURIComponent(server.version)}`;
178
+ return {
179
+ source: SOURCE,
180
+ kind: "mcp-server",
181
+ identityKey: `mcp-registry:${server.name.toLowerCase()}@${server.version}`,
182
+ ...(repositoryKey ? { repositoryKey } : {}),
183
+ externalId: `${server.name}@${server.version}`,
184
+ namespace,
185
+ name: server.name,
186
+ ...(typeof server.title === "string" ? { title: server.title } : {}),
187
+ description: server.description,
188
+ version: server.version,
189
+ sourceUrl,
190
+ ...(repository ? { repository } : {}),
191
+ distributions: [...packages, ...remotes],
192
+ verification: {
193
+ registry: "Official MCP Registry",
194
+ lifecycleStatus,
195
+ ...(typeof meta?.statusMessage === "string"
196
+ ? { statusMessage: meta.statusMessage }
197
+ : {}),
198
+ ...(typeof meta?.publishedAt === "string"
199
+ ? { publishedAt: meta.publishedAt }
200
+ : {}),
201
+ ...(typeof meta?.updatedAt === "string"
202
+ ? { updatedAt: meta.updatedAt }
203
+ : {}),
204
+ ...(typeof meta?.statusChangedAt === "string"
205
+ ? { statusChangedAt: meta.statusChangedAt }
206
+ : {}),
207
+ ...(typeof meta?.isLatest === "boolean"
208
+ ? { isLatest: meta.isLatest }
209
+ : {}),
210
+ namespaceEvidence: "The official registry records publisher namespace verification for publication; Loadout did not independently re-prove namespace ownership.",
211
+ meaning: "Registry membership establishes identity and distribution metadata only, not safety, popularity, availability, permissions, or performance.",
212
+ },
213
+ attribution: {
214
+ source: SOURCE,
215
+ sourceUrl: ATTRIBUTION,
216
+ observedAt,
217
+ meaning: "Metadata reported by the read-only Official MCP Registry v0.1 API; no server or package was installed or executed.",
218
+ },
219
+ };
220
+ }
221
+ function parsePage(value, pageSize) {
222
+ if (!isRecord(value) || !Array.isArray(value.servers))
223
+ throw new Error("Official MCP Registry response envelope is invalid");
224
+ if (value.servers.length > pageSize)
225
+ throw new Error("Official MCP Registry exceeded the requested page size");
226
+ if (value.metadata !== undefined && !isRecord(value.metadata))
227
+ throw new Error("Official MCP Registry pagination metadata is invalid");
228
+ const count = isRecord(value.metadata) ? value.metadata.count : undefined;
229
+ const nextCursor = isRecord(value.metadata)
230
+ ? value.metadata.nextCursor
231
+ : undefined;
232
+ if ((count !== undefined &&
233
+ (!Number.isSafeInteger(count) || count !== value.servers.length)) ||
234
+ (nextCursor !== undefined && !boundedString(nextCursor, 2_048)))
235
+ throw new Error("Official MCP Registry pagination metadata is invalid");
236
+ return {
237
+ servers: value.servers,
238
+ ...(typeof nextCursor === "string" && nextCursor ? { nextCursor } : {}),
239
+ };
240
+ }
241
+ export function isMcpRegistryDiscoveryRecord(value) {
242
+ if (!isRecord(value))
243
+ return false;
244
+ const record = value;
245
+ return (record.source === SOURCE &&
246
+ record.kind === "mcp-server" &&
247
+ boundedString(record.identityKey, 1_024) &&
248
+ boundedString(record.externalId, 512) &&
249
+ boundedString(record.namespace, 200) &&
250
+ boundedString(record.name, 200) &&
251
+ SERVER_NAME.test(record.name) &&
252
+ boundedString(record.description, 100, { multiline: true }) &&
253
+ boundedString(record.version, 255) &&
254
+ boundedString(record.sourceUrl, 2_048) &&
255
+ Array.isArray(record.distributions) &&
256
+ record.distributions.length <= 100 &&
257
+ isRecord(record.verification) &&
258
+ isRecord(record.attribution) &&
259
+ record.attribution.source === SOURCE &&
260
+ validDate(record.attribution.observedAt));
261
+ }
262
+ function issueFromError(error, page) {
263
+ if (error instanceof ConnectorRequestError)
264
+ return {
265
+ code: error.status === 429 ? "rate-limited" : "request-failed",
266
+ message: `Official MCP Registry page ${page} failed: ${error.message}`,
267
+ page,
268
+ ...(error.retryAfterSeconds !== undefined
269
+ ? { retryAfterSeconds: error.retryAfterSeconds }
270
+ : {}),
271
+ };
272
+ return {
273
+ code: "invalid-response",
274
+ message: `Official MCP Registry page ${page} was invalid: ${error instanceof Error ? error.message : String(error)}`,
275
+ page,
276
+ };
277
+ }
278
+ async function cachedOrUnavailable(cachePath, fetchedAt, issues) {
279
+ const cachedPath = cachePath === false ? undefined : cachePath;
280
+ const cache = cachedPath === undefined
281
+ ? undefined
282
+ : await readDiscoveryConnectorCache(cachedPath, SOURCE, isMcpRegistryDiscoveryRecord);
283
+ return cache
284
+ ? {
285
+ source: SOURCE,
286
+ status: "cached",
287
+ fetchedAt,
288
+ records: cache.records,
289
+ pagesFetched: 0,
290
+ issues,
291
+ cache: { path: cachedPath, cachedAt: cache.cachedAt },
292
+ }
293
+ : {
294
+ source: SOURCE,
295
+ status: "unavailable",
296
+ fetchedAt,
297
+ records: [],
298
+ pagesFetched: 0,
299
+ issues,
300
+ };
301
+ }
302
+ /** Read public identity/distribution metadata from the official v0.1 registry. */
303
+ export async function discoverOfficialMcpRegistry(options = {}) {
304
+ const now = options.now ?? new Date();
305
+ const fetchedAt = now.toISOString();
306
+ const pageSize = Math.max(1, Math.min(options.pageSize ?? 100, 100));
307
+ const maxPages = Math.max(1, Math.min(options.maxPages ?? 5, 10));
308
+ const maxRecords = Math.max(1, Math.min(options.maxRecords ?? 500, 1_000));
309
+ const search = options.search?.trim();
310
+ if (search && (!boundedString(search, 200) || search.length < 2))
311
+ throw new Error("Official MCP Registry search must be 2-200 safe characters");
312
+ if (options.updatedSince !== undefined &&
313
+ (!validDate(options.updatedSince) || options.updatedSince.length > 64))
314
+ throw new Error("Official MCP Registry updatedSince must be an RFC3339 date");
315
+ if (options.cursor !== undefined && !boundedString(options.cursor, 2_048))
316
+ throw new Error("Official MCP Registry cursor is invalid");
317
+ const cachePath = options.cachePath === false
318
+ ? false
319
+ : (options.cachePath ??
320
+ join(loadoutHome(), "discovery-cache", "official-mcp-registry.json"));
321
+ const records = [];
322
+ const positions = new Map();
323
+ const issues = [];
324
+ const seenCursors = new Set();
325
+ let cursor = options.cursor;
326
+ let pagesFetched = 0;
327
+ let stoppedByFailure = false;
328
+ let stoppedByRecordLimit = false;
329
+ let truncatedWithinPage = false;
330
+ let rateLimit;
331
+ for (let page = 0; page < maxPages && records.length < maxRecords; page++) {
332
+ if (cursor) {
333
+ if (seenCursors.has(cursor)) {
334
+ issues.push({
335
+ code: "pagination-replay",
336
+ message: "Official MCP Registry repeated a pagination cursor; traversal stopped",
337
+ page,
338
+ });
339
+ stoppedByFailure = true;
340
+ break;
341
+ }
342
+ seenCursors.add(cursor);
343
+ }
344
+ const url = new URL(API);
345
+ url.searchParams.set("limit", String(pageSize));
346
+ if (cursor)
347
+ url.searchParams.set("cursor", cursor);
348
+ if (search)
349
+ url.searchParams.set("search", search);
350
+ if (options.updatedSince)
351
+ url.searchParams.set("updated_since", options.updatedSince);
352
+ if (options.latestOnly !== false)
353
+ url.searchParams.set("version", "latest");
354
+ if (options.includeDeleted)
355
+ url.searchParams.set("include_deleted", "true");
356
+ try {
357
+ const response = await fetchBoundedJson(url.toString(), {
358
+ fetcher: options.fetcher,
359
+ // The public registry can take slightly over ten seconds from a cold
360
+ // edge. Keep the request bounded while avoiding false offline results.
361
+ timeoutMs: options.timeoutMs ?? 20_000,
362
+ maximumBytes: 8 * 1024 * 1024,
363
+ headers: {
364
+ accept: "application/json",
365
+ "user-agent": "loadout-ai-discovery",
366
+ },
367
+ });
368
+ pagesFetched++;
369
+ rateLimit = rateLimitFromHeaders(response.headers) ?? rateLimit;
370
+ const parsed = parsePage(response.value, pageSize);
371
+ for (const [index, value] of parsed.servers.entries()) {
372
+ const normalized = normalizeServer(value, fetchedAt);
373
+ if (!normalized) {
374
+ issues.push({
375
+ code: "invalid-record",
376
+ message: `Official MCP Registry page ${page} record ${index + 1} was skipped because it failed the bounded v0.1 metadata schema`,
377
+ page,
378
+ });
379
+ continue;
380
+ }
381
+ const existingIndex = positions.get(normalized.identityKey);
382
+ if (existingIndex !== undefined) {
383
+ const existing = records[existingIndex];
384
+ const existingUpdated = Date.parse(existing.verification.updatedAt ??
385
+ existing.verification.publishedAt ??
386
+ new Date(0).toISOString());
387
+ const candidateUpdated = Date.parse(normalized.verification.updatedAt ??
388
+ normalized.verification.publishedAt ??
389
+ new Date(0).toISOString());
390
+ if (candidateUpdated > existingUpdated)
391
+ records[existingIndex] = normalized;
392
+ issues.push({
393
+ code: "duplicate-record",
394
+ message: `Official MCP Registry repeated ${normalized.externalId}; the most recently updated copy was retained`,
395
+ page,
396
+ });
397
+ continue;
398
+ }
399
+ positions.set(normalized.identityKey, records.length);
400
+ records.push(normalized);
401
+ if (records.length >= maxRecords) {
402
+ truncatedWithinPage = index < parsed.servers.length - 1;
403
+ stoppedByRecordLimit =
404
+ truncatedWithinPage || Boolean(parsed.nextCursor);
405
+ break;
406
+ }
407
+ }
408
+ cursor = truncatedWithinPage ? undefined : parsed.nextCursor;
409
+ if (!cursor)
410
+ break;
411
+ }
412
+ catch (error) {
413
+ issues.push(issueFromError(error, page));
414
+ stoppedByFailure = true;
415
+ break;
416
+ }
417
+ }
418
+ if (!stoppedByFailure && stoppedByRecordLimit)
419
+ issues.push({
420
+ code: "pagination-limit",
421
+ message: `Official MCP Registry pagination stopped at the configured ${maxRecords}-record limit${truncatedWithinPage ? "; no continuation is exposed because the limit was reached within a page" : ""}`,
422
+ page: pagesFetched,
423
+ });
424
+ else if (!stoppedByFailure && cursor && pagesFetched >= maxPages)
425
+ issues.push({
426
+ code: "pagination-limit",
427
+ message: `Official MCP Registry pagination stopped at the configured ${maxPages}-page limit`,
428
+ page: pagesFetched,
429
+ });
430
+ if (!records.length && issues.length)
431
+ return cachedOrUnavailable(cachePath, fetchedAt, issues);
432
+ let status = issues.length ? "partial" : "complete";
433
+ if (status === "complete" && cachePath !== false) {
434
+ try {
435
+ await writeDiscoveryConnectorCache(cachePath, SOURCE, records, now);
436
+ }
437
+ catch (error) {
438
+ status = "partial";
439
+ issues.push({
440
+ code: "cache-write-failed",
441
+ message: `Official MCP Registry metadata was fetched but its offline cache could not be written: ${error instanceof Error ? error.message : String(error)}`,
442
+ });
443
+ }
444
+ }
445
+ return {
446
+ source: SOURCE,
447
+ status,
448
+ fetchedAt,
449
+ records,
450
+ pagesFetched,
451
+ issues,
452
+ ...(cursor ? { next: cursor } : {}),
453
+ ...(rateLimit ? { rateLimit } : {}),
454
+ };
455
+ }