@@ -191,6 +191,19 @@ public static bool IsNodeInstalled()
191191 public static bool HasSep2243Scenarios ( )
192192 => HasInstalledConformanceVersionAtLeast ( new Version ( 0 , 2 , 0 ) ) ;
193193
194+ /// <summary>
195+ /// Checks whether the installed conformance package contains a spec-conformant
196+ /// <c>http-custom-headers</c> scenario. Prereleases 0.2.0-alpha.5 through 0.2.0-alpha.7
197+ /// annotated a <c>number</c>-typed parameter with <c>x-mcp-header</c>, which SEP-2243
198+ /// forbids; a conformant client excludes that tool, so every positive check in the
199+ /// scenario fails. Conformance PR #371 fixed the scenario and shipped it in 0.2.0-alpha.8,
200+ /// so this gate requires at least that version. Unlike <see cref="HasSep2243Scenarios"/>,
201+ /// this comparison honors the semver prerelease so older 0.2.0 prereleases are skipped
202+ /// rather than failing spuriously.
203+ /// </summary>
204+ public static bool HasConformantCustomHeadersScenario ( )
205+ => IsInstalledConformanceVersionAtLeast ( "0.2.0-alpha.8" ) ;
206+
194207 /// <summary>
195208 /// Checks whether the SEP-2549 "caching" conformance scenario (added in conformance
196209 /// PR #275) is available, by reading the <em>installed</em> conformance package version
@@ -256,7 +269,138 @@ private static bool HasInstalledConformanceVersionAtLeast(Version minimumVersion
256269 }
257270
258271 /// <summary>
259- /// Runs the conformance runner ("conformance <arguments>") in server mode and returns
272+ /// Returns <see langword="true"/> when the conformance package installed in node_modules
273+ /// has a semver precedence greater than or equal to <paramref name="minimumVersion"/>,
274+ /// honoring the prerelease component (e.g. "0.2.0-alpha.8"). Returns <see langword="false"/>
275+ /// when no version can be determined.
276+ /// </summary>
277+ private static bool IsInstalledConformanceVersionAtLeast ( string minimumVersion )
278+ {
279+ var installed = GetInstalledConformanceVersionString ( ) ;
280+ return installed is not null && CompareSemVer ( installed , minimumVersion ) >= 0 ;
281+ }
282+
283+ /// <summary>
284+ /// Reads the raw version string of the conformance package installed in node_modules,
285+ /// preserving any prerelease/build suffix. Returns <see langword="null"/> if it cannot be
286+ /// determined.
287+ /// </summary>
288+ private static string ? GetInstalledConformanceVersionString ( )
289+ {
290+ try
291+ {
292+ var repoRoot = FindRepoRoot ( ) ;
293+ var packageJsonPath = Path . Combine (
294+ repoRoot , "node_modules" , "@modelcontextprotocol" , "conformance" , "package.json" ) ;
295+
296+ if ( ! File . Exists ( packageJsonPath ) )
297+ {
298+ return null ;
299+ }
300+
301+ using var json = System . Text . Json . JsonDocument . Parse ( File . ReadAllText ( packageJsonPath ) ) ;
302+ if ( json . RootElement . TryGetProperty ( "version" , out var versionElement ) )
303+ {
304+ return versionElement . GetString ( ) ;
305+ }
306+
307+ return null ;
308+ }
309+ catch
310+ {
311+ return null ;
312+ }
313+ }
314+
315+ /// <summary>
316+ /// Compares two semantic version strings by precedence, honoring the prerelease component
317+ /// per the SemVer 2.0.0 rules used here (numeric identifiers compare numerically, a version
318+ /// with a prerelease has lower precedence than the same version without one, and a shorter
319+ /// set of prerelease identifiers has lower precedence when all preceding ones are equal).
320+ /// Build metadata (after '+') is ignored. Returns a negative value when <paramref name="a"/>
321+ /// precedes <paramref name="b"/>, zero when equal, and a positive value otherwise.
322+ /// </summary>
323+ private static int CompareSemVer ( string a , string b )
324+ {
325+ var ( coreA , preA ) = SplitSemVer ( a ) ;
326+ var ( coreB , preB ) = SplitSemVer ( b ) ;
327+
328+ var coreCompare = coreA . CompareTo ( coreB ) ;
329+ if ( coreCompare != 0 )
330+ {
331+ return coreCompare ;
332+ }
333+
334+ // A version without a prerelease outranks one with a prerelease.
335+ if ( preA . Length == 0 && preB . Length == 0 )
336+ {
337+ return 0 ;
338+ }
339+ if ( preA . Length == 0 )
340+ {
341+ return 1 ;
342+ }
343+ if ( preB . Length == 0 )
344+ {
345+ return - 1 ;
346+ }
347+
348+ var count = Math . Min ( preA . Length , preB . Length ) ;
349+ for ( var i = 0 ; i < count ; i ++ )
350+ {
351+ var idA = preA [ i ] ;
352+ var idB = preB [ i ] ;
353+ var numA = int . TryParse ( idA , out var na ) ;
354+ var numB = int . TryParse ( idB , out var nb ) ;
355+
356+ int cmp ;
357+ if ( numA && numB )
358+ {
359+ cmp = na . CompareTo ( nb ) ;
360+ }
361+ else if ( numA )
362+ {
363+ // Numeric identifiers always have lower precedence than alphanumeric ones.
364+ cmp = - 1 ;
365+ }
366+ else if ( numB )
367+ {
368+ cmp = 1 ;
369+ }
370+ else
371+ {
372+ cmp = string . CompareOrdinal ( idA , idB ) ;
373+ }
374+
375+ if ( cmp != 0 )
376+ {
377+ return cmp ;
378+ }
379+ }
380+
381+ return preA . Length . CompareTo ( preB . Length ) ;
382+ }
383+
384+ /// <summary>
385+ /// Splits a semver string into its numeric core (major.minor.patch) and its prerelease
386+ /// identifiers, ignoring any build metadata after '+'. Missing core components default to 0.
387+ /// </summary>
388+ private static ( Version Core , string [ ] Prerelease ) SplitSemVer ( string version )
389+ {
390+ var withoutBuild = version . Split ( new [ ] { '+' } , 2 ) [ 0 ] ;
391+ var parts = withoutBuild . Split ( new [ ] { '-' } , 2 ) ;
392+ var prerelease = parts . Length > 1 && parts [ 1 ] . Length > 0
393+ ? parts [ 1 ] . Split ( '.' )
394+ : Array . Empty < string > ( ) ;
395+
396+ var coreParts = parts [ 0 ] . Split ( '.' ) ;
397+ int Part ( int index ) => index < coreParts . Length && int . TryParse ( coreParts [ index ] , out var v ) ? v : 0 ;
398+ var core = new Version ( Part ( 0 ) , Part ( 1 ) , Part ( 2 ) ) ;
399+
400+ return ( core , prerelease ) ;
401+ }
402+
403+
260404 /// whether it succeeded along with the captured stdout/stderr. Centralizes the process
261405 /// plumbing (output capture, a 5-minute timeout, and the Windows libuv-shutdown fallback)
262406 /// shared by the server-side conformance tests.
0 commit comments