From 3a3b9bcd87c1e93311e60a28e375eae072ce1dd4 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Sat, 7 Nov 2026 02:51:22 +0000 Subject: [PATCH] fix(mcp-registry): Ignore empty values the registry drops when verifying a listing The MCP Registry stores server.json with Go omitempty, so fields such as isRequired: false or isSecret: false are missing from the listing. The strict comparison then reported a mismatch right after a successful publish, and a retry failed the same way because the version is now listed. --- src/targets/__tests__/mcpRegistry.test.ts | 25 +++++++++++++ src/targets/mcpRegistry.ts | 45 ++++++++++++++++++++--- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/targets/__tests__/mcpRegistry.test.ts b/src/targets/__tests__/mcpRegistry.test.ts index 133c02fa..7e52d069 100644 --- a/src/targets/__tests__/mcpRegistry.test.ts +++ b/src/targets/__tests__/mcpRegistry.test.ts @@ -457,6 +457,31 @@ describe('existing listing', () => { ]); }); + test('ignores empty values that the registry drops', async () => { + const pkg = { + ...MANIFEST.packages[0], + environmentVariables: [ + { name: 'TOKEN', isRequired: false, isSecret: true, value: '' }, + ], + packageArguments: [], + }; + writeManifest({ ...MANIFEST, packages: [pkg] }); + const listedPackage = { + ...MANIFEST.packages[0], + version: VERSION, + environmentVariables: [{ name: 'TOKEN', isSecret: true }], + }; + const fetchMock = mockFetch({ + [`GET ${LISTING_URL}`]: json( + listing({ ...EXPECTED_SERVER, packages: [listedPackage] }), + ), + }); + + await createTarget().publish(VERSION, 'sha'); + + expect(requests(fetchMock)).toHaveLength(1); + }); + test('fails when the listed version differs from the manifest', async () => { const fetchMock = mockFetch({ [`GET ${LISTING_URL}`]: json( diff --git a/src/targets/mcpRegistry.ts b/src/targets/mcpRegistry.ts index 43a1f7f5..a59a27cd 100644 --- a/src/targets/mcpRegistry.ts +++ b/src/targets/mcpRegistry.ts @@ -290,8 +290,9 @@ export class McpRegistryTarget extends BaseTarget { } /** - * Checks that the listing equals the manifest (ignoring `$schema`) and is - * active. Throws `mismatchMessage` when the content differs. + * Checks that the listing equals the manifest (ignoring `$schema` and the + * empty values the registry drops) and is active. Throws `mismatchMessage` + * when the content differs. */ private verifyListing( listing: RegistryListing, @@ -300,8 +301,8 @@ export class McpRegistryTarget extends BaseTarget { ): void { if ( !isDeepStrictEqual( - withoutSchema(listing.server ?? {}), - withoutSchema(manifest), + normalizeServer(listing.server ?? {}), + normalizeServer(manifest), ) ) { throw new Error(mismatchMessage); @@ -475,12 +476,44 @@ export class McpRegistryTarget extends BaseTarget { } } -function withoutSchema( +/** + * Returns the server data the registry stores for `server`, for comparison: + * without `$schema`, and without the empty values (`false`, `""`, `null`, + * `[]`, `{}`) that the registry drops from its listings (Go `omitempty`). + */ +function normalizeServer( server: Record, ): Record { const copy = { ...server }; delete copy.$schema; - return copy; + return withoutEmptyValues(copy) as Record; +} + +function withoutEmptyValues(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(withoutEmptyValues); + } + if (value === null || typeof value !== 'object') { + return value; + } + const result: Record = {}; + for (const [key, item] of Object.entries(value)) { + const normalized = withoutEmptyValues(item); + if (!isEmptyValue(normalized)) { + result[key] = normalized; + } + } + return result; +} + +function isEmptyValue(value: unknown): boolean { + if (value === undefined || value === null || value === false) { + return true; + } + if (typeof value === 'string' || Array.isArray(value)) { + return value.length === 0; + } + return typeof value === 'object' && Object.keys(value).length === 0; } function errorMessage(err: unknown): string {