Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
This commit is contained in:
parent
3e7ca32130
commit
b2a40128fc
4 changed files with 168 additions and 10 deletions
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -5,7 +5,7 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/conformance": "0.2.0-alpha.5",
|
||||
"@modelcontextprotocol/conformance": "0.2.0-alpha.8",
|
||||
"@modelcontextprotocol/server-everything": "2026.1.26",
|
||||
"@modelcontextprotocol/server-memory": "2026.1.26"
|
||||
}
|
||||
|
|
@ -23,9 +23,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/conformance": {
|
||||
"version": "0.2.0-alpha.5",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/conformance/-/conformance-0.2.0-alpha.5.tgz",
|
||||
"integrity": "sha512-sYxNHKk/m7Vx0/XxKulbcmgqz7wH2tXIge9u1G1CzpluaZHTci966m5dw7wGyQKx7IR3/V+/hAAGXc8ZqBMoyw==",
|
||||
"version": "0.2.0-alpha.8",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/conformance/-/conformance-0.2.0-alpha.8.tgz",
|
||||
"integrity": "sha512-ktbvq6ftvs23TjZ/WVmTKZHue8dqoUF5bBG3Qd5pbawfTyuveZw93o07uQij8tslBlccpPVS5jE212G+SnLo2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"private": true,
|
||||
"description": "Pinned npm dependencies for MCP C# SDK integration and conformance tests",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/conformance": "0.2.0-alpha.5",
|
||||
"@modelcontextprotocol/conformance": "0.2.0-alpha.8",
|
||||
"@modelcontextprotocol/server-everything": "2026.1.26",
|
||||
"@modelcontextprotocol/server-memory": "2026.1.26"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,19 @@ public static class NodeHelpers
|
|||
public static bool HasSep2243Scenarios()
|
||||
=> HasInstalledConformanceVersionAtLeast(new Version(0, 2, 0));
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the installed conformance package contains a spec-conformant
|
||||
/// <c>http-custom-headers</c> scenario. Prereleases 0.2.0-alpha.5 through 0.2.0-alpha.7
|
||||
/// annotated a <c>number</c>-typed parameter with <c>x-mcp-header</c>, which SEP-2243
|
||||
/// forbids; a conformant client excludes that tool, so every positive check in the
|
||||
/// scenario fails. Conformance PR #371 fixed the scenario and shipped it in 0.2.0-alpha.8,
|
||||
/// so this gate requires at least that version. Unlike <see cref="HasSep2243Scenarios"/>,
|
||||
/// this comparison honors the semver prerelease so older 0.2.0 prereleases are skipped
|
||||
/// rather than failing spuriously.
|
||||
/// </summary>
|
||||
public static bool HasConformantCustomHeadersScenario()
|
||||
=> IsInstalledConformanceVersionAtLeast("0.2.0-alpha.8");
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the SEP-2549 "caching" conformance scenario (added in conformance
|
||||
/// PR #275) is available, by reading the <em>installed</em> conformance package version
|
||||
|
|
@ -256,7 +269,138 @@ public static class NodeHelpers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the conformance runner ("conformance <arguments>") in server mode and returns
|
||||
/// Returns <see langword="true"/> when the conformance package installed in node_modules
|
||||
/// has a semver precedence greater than or equal to <paramref name="minimumVersion"/>,
|
||||
/// honoring the prerelease component (e.g. "0.2.0-alpha.8"). Returns <see langword="false"/>
|
||||
/// when no version can be determined.
|
||||
/// </summary>
|
||||
private static bool IsInstalledConformanceVersionAtLeast(string minimumVersion)
|
||||
{
|
||||
var installed = GetInstalledConformanceVersionString();
|
||||
return installed is not null && CompareSemVer(installed, minimumVersion) >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the raw version string of the conformance package installed in node_modules,
|
||||
/// preserving any prerelease/build suffix. Returns <see langword="null"/> if it cannot be
|
||||
/// determined.
|
||||
/// </summary>
|
||||
private static string? GetInstalledConformanceVersionString()
|
||||
{
|
||||
try
|
||||
{
|
||||
var repoRoot = FindRepoRoot();
|
||||
var packageJsonPath = Path.Combine(
|
||||
repoRoot, "node_modules", "@modelcontextprotocol", "conformance", "package.json");
|
||||
|
||||
if (!File.Exists(packageJsonPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var json = System.Text.Json.JsonDocument.Parse(File.ReadAllText(packageJsonPath));
|
||||
if (json.RootElement.TryGetProperty("version", out var versionElement))
|
||||
{
|
||||
return versionElement.GetString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two semantic version strings by precedence, honoring the prerelease component
|
||||
/// per the SemVer 2.0.0 rules used here (numeric identifiers compare numerically, a version
|
||||
/// with a prerelease has lower precedence than the same version without one, and a shorter
|
||||
/// set of prerelease identifiers has lower precedence when all preceding ones are equal).
|
||||
/// Build metadata (after '+') is ignored. Returns a negative value when <paramref name="a"/>
|
||||
/// precedes <paramref name="b"/>, zero when equal, and a positive value otherwise.
|
||||
/// </summary>
|
||||
private static int CompareSemVer(string a, string b)
|
||||
{
|
||||
var (coreA, preA) = SplitSemVer(a);
|
||||
var (coreB, preB) = SplitSemVer(b);
|
||||
|
||||
var coreCompare = coreA.CompareTo(coreB);
|
||||
if (coreCompare != 0)
|
||||
{
|
||||
return coreCompare;
|
||||
}
|
||||
|
||||
// A version without a prerelease outranks one with a prerelease.
|
||||
if (preA.Length == 0 && preB.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (preA.Length == 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
if (preB.Length == 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var count = Math.Min(preA.Length, preB.Length);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var idA = preA[i];
|
||||
var idB = preB[i];
|
||||
var numA = int.TryParse(idA, out var na);
|
||||
var numB = int.TryParse(idB, out var nb);
|
||||
|
||||
int cmp;
|
||||
if (numA && numB)
|
||||
{
|
||||
cmp = na.CompareTo(nb);
|
||||
}
|
||||
else if (numA)
|
||||
{
|
||||
// Numeric identifiers always have lower precedence than alphanumeric ones.
|
||||
cmp = -1;
|
||||
}
|
||||
else if (numB)
|
||||
{
|
||||
cmp = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
cmp = string.CompareOrdinal(idA, idB);
|
||||
}
|
||||
|
||||
if (cmp != 0)
|
||||
{
|
||||
return cmp;
|
||||
}
|
||||
}
|
||||
|
||||
return preA.Length.CompareTo(preB.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a semver string into its numeric core (major.minor.patch) and its prerelease
|
||||
/// identifiers, ignoring any build metadata after '+'. Missing core components default to 0.
|
||||
/// </summary>
|
||||
private static (Version Core, string[] Prerelease) SplitSemVer(string version)
|
||||
{
|
||||
var withoutBuild = version.Split(new[] { '+' }, 2)[0];
|
||||
var parts = withoutBuild.Split(new[] { '-' }, 2);
|
||||
var prerelease = parts.Length > 1 && parts[1].Length > 0
|
||||
? parts[1].Split('.')
|
||||
: Array.Empty<string>();
|
||||
|
||||
var coreParts = parts[0].Split('.');
|
||||
int Part(int index) => index < coreParts.Length && int.TryParse(coreParts[index], out var v) ? v : 0;
|
||||
var core = new Version(Part(0), Part(1), Part(2));
|
||||
|
||||
return (core, prerelease);
|
||||
}
|
||||
|
||||
|
||||
/// whether it succeeded along with the captured stdout/stderr. Centralizes the process
|
||||
/// plumbing (output capture, a 5-minute timeout, and the Windows libuv-shutdown fallback)
|
||||
/// shared by the server-side conformance tests.
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ public class ClientConformanceTests
|
|||
// Public static property required for SkipUnless attribute
|
||||
public static bool IsNodeInstalled => NodeHelpers.IsNodeInstalled();
|
||||
public static bool HasSep2243Scenarios => NodeHelpers.HasSep2243Scenarios();
|
||||
public static bool HasConformantCustomHeadersScenario => NodeHelpers.HasConformantCustomHeadersScenario();
|
||||
|
||||
public ClientConformanceTests(ITestOutputHelper output)
|
||||
{
|
||||
|
|
@ -66,10 +67,6 @@ public class ClientConformanceTests
|
|||
[Theory(Skip = "SEP-2243 conformance scenarios not available (requires conformance package >= 0.2.0).", SkipUnless = nameof(HasSep2243Scenarios))]
|
||||
[InlineData("http-standard-headers")]
|
||||
[InlineData("http-invalid-tool-headers")]
|
||||
// Commented out: the upstream scenario annotates a "number"-typed parameter with x-mcp-header,
|
||||
// which SEP-2243 forbids, so the client rejects the tool and sends no Mcp-Param-* headers,
|
||||
// failing every positive check. Re-enable once a conformant conformance package ships (#1655).
|
||||
// [InlineData("http-custom-headers")]
|
||||
public async Task RunConformanceTest_Sep2243(string scenario)
|
||||
{
|
||||
// Run the conformance test suite
|
||||
|
|
@ -80,6 +77,23 @@ public class ClientConformanceTests
|
|||
$"Conformance test failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}");
|
||||
}
|
||||
|
||||
// The http-custom-headers scenario needs a tighter gate than the other SEP-2243 scenarios:
|
||||
// conformance 0.2.0-alpha.5 through 0.2.0-alpha.7 shipped it with an x-mcp-header on a
|
||||
// number-typed parameter (forbidden by SEP-2243), which a conformant client excludes,
|
||||
// failing every positive check. It was fixed upstream in 0.2.0-alpha.8 (conformance #371),
|
||||
// so require at least that version to avoid spurious failures on older 0.2.0 prereleases.
|
||||
[Theory(Skip = "Conformant http-custom-headers scenario not available (requires conformance package >= 0.2.0-alpha.8).", SkipUnless = nameof(HasConformantCustomHeadersScenario))]
|
||||
[InlineData("http-custom-headers")]
|
||||
public async Task RunConformanceTest_Sep2243_CustomHeaders(string scenario)
|
||||
{
|
||||
// Run the conformance test suite
|
||||
var result = await RunClientConformanceScenario(scenario);
|
||||
|
||||
// Report the results
|
||||
Assert.True(result.Success,
|
||||
$"Conformance test failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}");
|
||||
}
|
||||
|
||||
private async Task<(bool Success, string Output, string Error)> RunClientConformanceScenario(string scenario)
|
||||
{
|
||||
// Construct an absolute path to the conformance client executable
|
||||
|
|
|
|||
Loading…
Reference in a new issue