From 3be37dd828e50a5d03c5916bef35b9f5a2afbfc6 Mon Sep 17 00:00:00 2001 From: Zdenek Srejber Date: Wed, 27 May 2026 11:16:47 +0200 Subject: [PATCH 1/5] feat: enhance path normalization for windows paths and add corresponding tests --- src/TALXIS.CLI.MCP/McpPathNormalizer.cs | 77 ++++++++++++++++++- .../MCP/McpPathNormalizerTests.cs | 47 +++++++++++ .../TALXIS.CLI.Tests/MCP/RootsServiceTests.cs | 35 ++++++++- 3 files changed, 154 insertions(+), 5 deletions(-) diff --git a/src/TALXIS.CLI.MCP/McpPathNormalizer.cs b/src/TALXIS.CLI.MCP/McpPathNormalizer.cs index 4583549..636d201 100644 --- a/src/TALXIS.CLI.MCP/McpPathNormalizer.cs +++ b/src/TALXIS.CLI.MCP/McpPathNormalizer.cs @@ -7,6 +7,10 @@ public static string NormalizeOperationalPath(string path, bool allowFileUriLoca if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Path must not be empty.", nameof(path)); + var expanded = ExpandDriveQualifiedHomePath(path); + if (expanded != null) + return Path.GetFullPath(expanded); + return Path.GetFullPath(ExpandHomeRelativePath(path, allowFileUriLocalPathHome)); } @@ -19,18 +23,83 @@ internal static string ExpandHomeRelativePath(string path, bool allowFileUriLoca if (suffixStart < 0) return path; + return TryExpandHomePath(path[suffixStart..]) ?? path; + } + + internal static string? ExpandDriveQualifiedHomePath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return null; + + var driveQualifiedHomeRemainder = TryGetDriveQualifiedHomeRemainder(path); + if (driveQualifiedHomeRemainder != null) + return TryExpandHomePath(driveQualifiedHomeRemainder); + + var fileUriDriveQualifiedHomeRemainder = TryGetFileUriDriveQualifiedHomeRemainder(path); + if (fileUriDriveQualifiedHomeRemainder != null) + return TryExpandHomePath(fileUriDriveQualifiedHomeRemainder); + + return TryNormalizeWindowsFileUriDrivePath(path); + } + + private static string? TryExpandHomePath(string remainder) + { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); if (string.IsNullOrWhiteSpace(home)) - return path; + return null; - var remainder = path[suffixStart..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, '\\', '/'); - if (string.IsNullOrEmpty(remainder)) + var trimmedRemainder = remainder.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, '\\', '/'); + if (string.IsNullOrEmpty(trimmedRemainder)) return home; - var segments = remainder.Split(['\\', '/'], StringSplitOptions.RemoveEmptyEntries); + var segments = trimmedRemainder.Split(['\\', '/'], StringSplitOptions.RemoveEmptyEntries); return segments.Length == 0 ? home : Path.Combine([home, .. segments]); } + private static string? TryGetDriveQualifiedHomeRemainder(string path) + { + if (!OperatingSystem.IsWindows()) + return null; + + if (!IsDriveQualifiedPath(path)) + return null; + + if (path[2] == '~') + return path[3..]; + + if (path.Length >= 4 && IsDirectorySeparator(path[2]) && path[3] == '~') + return path[4..]; + + return null; + } + + private static string? TryGetFileUriDriveQualifiedHomeRemainder(string path) + { + if (!OperatingSystem.IsWindows()) + return null; + + if (path.Length < 5 || path[0] != '/' || !char.IsLetter(path[1]) || path[2] != ':' || !IsDirectorySeparator(path[3]) || path[4] != '~') + return null; + + return path[5..]; + } + + private static string? TryNormalizeWindowsFileUriDrivePath(string path) + { + if (OperatingSystem.IsWindows() && path.Length >= 3 + && path[0] == '/' && char.IsLetter(path[1]) && path[2] == ':') + { + return path[1..]; + } + + return null; + } + + private static bool IsDriveQualifiedPath(string path) + { + return path.Length >= 3 && char.IsLetter(path[0]) && path[1] == ':'; + } + private static int GetHomeRelativeSuffixStart(string path, bool allowFileUriLocalPathHome) { if (path == "~") diff --git a/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs b/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs index 86cfc8f..5ea19ee 100644 --- a/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs +++ b/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs @@ -23,6 +23,29 @@ public void NormalizeOperationalPath_HomeRelativeInput_ResolvesAgainstUserProfil Assert.Equal(expected, result); } + [Theory] + [InlineData("C:~")] + [InlineData("C:/~")] + [InlineData("C:\\~")] + [InlineData("C:/~/Sources/project")] + [InlineData("C:\\~\\Sources\\project")] + [InlineData("c:/~")] + [InlineData("c:/~/Sources/project")] + [InlineData("c:\\~\\Sources\\project")] + [InlineData("/C:/~/Sources/project")] + [InlineData("/c:/~/Sources/project")] + public void NormalizeOperationalPath_DriveQualifiedHome_UsesUserProfile(string input) + { + var home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile); + var result = McpPathNormalizer.NormalizeOperationalPath(input); + var expected = string.IsNullOrWhiteSpace(home) + ? Path.GetFullPath(input) + : input.EndsWith("~") || input.EndsWith("~/") || input.EndsWith("~\\") + ? Path.GetFullPath(home) + : Path.GetFullPath(Path.Combine(home, "Sources", "project")); + Assert.Equal(expected, result); + } + [Theory] [InlineData("/~/Sources/project")] [InlineData("\\~\\Sources\\project")] @@ -38,6 +61,30 @@ public void NormalizeOperationalPath_FileUriLocalHomePath_ResolvesAgainstUserPro Assert.Equal(expected, result); } + [Theory] + [InlineData("/C:/Users/project")] + [InlineData("/c:/Users/project")] + public void NormalizeOperationalPath_FileUriDrivePath_NormalizesLeadingSlash(string input) + { + var result = McpPathNormalizer.NormalizeOperationalPath(input); + var expected = OperatingSystem.IsWindows() + ? Path.GetFullPath(input[1..]) + : Path.GetFullPath(input); + Assert.Equal(expected, result); + } + + [Fact] + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + public void NormalizeOperationalPath_VsCodeLowercaseDriveUri_IsValidOnWindows() + { + if (!OperatingSystem.IsWindows()) return; + var localPath = "/c:/Users/example-user/Sources/project"; + var result = McpPathNormalizer.NormalizeOperationalPath(localPath, allowFileUriLocalPathHome: true); + Assert.True(Path.IsPathRooted(result)); + Assert.DoesNotContain("c:\\c:", result, StringComparison.OrdinalIgnoreCase); + Assert.Equal(Path.GetFullPath("c:/Users/example-user/Sources/project"), result); + } + [Fact] public void NormalizeOperationalPath_AbsoluteUnixPath_RemainsAbsolute() { diff --git a/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs b/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs index f58d912..2224718 100644 --- a/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs +++ b/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs @@ -32,7 +32,25 @@ public void ConvertFileUri_WindowsLowercaseDrive_ReturnsNormalisedPath() // VS Code on Windows sends lowercase drive letters var result = RootsService.ConvertFileUriToPath("file:///c:/Users/project"); Assert.NotNull(result); - Assert.EndsWith("c:/Users/project", result.Replace('\\', '/')); + Assert.True(Path.IsPathFullyQualified(result), $"Expected fully-qualified path but got: {result}"); + + var normalised = result.Replace('\\', '/'); + Assert.DoesNotContain("c:/c:/", normalised, StringComparison.OrdinalIgnoreCase); + Assert.Contains("Users/project", normalised, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + [System.Runtime.Versioning.SupportedOSPlatform("windows")] + public void ConvertFileUri_LowercaseDriveWorkspaceRoot_DoesNotDuplicateDrive() + { + if (!OperatingSystem.IsWindows()) return; + + var result = RootsService.ConvertFileUriToPath("file:///c:/Users/example-user/Sources/my-agent-team"); + Assert.NotNull(result); + Assert.True(Path.IsPathFullyQualified(result)); + Assert.DoesNotContain(@"c:\c:", result, StringComparison.OrdinalIgnoreCase); + Assert.EndsWith(Path.Combine("Users", "example-user", "Sources", "my-agent-team"), result, + StringComparison.OrdinalIgnoreCase); } [Fact] @@ -79,6 +97,21 @@ public void ConvertFileUri_HomeRelativePath_ResolvesAgainstUserProfile() Assert.Equal(expected, result); } + [Theory] + [InlineData("file:///C:/~/Sources/project")] + [InlineData("file:///c:/~/Sources/project")] + public void ConvertFileUri_DriveQualifiedHome_UsesUserProfile(string uri) + { + var home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile); + var result = RootsService.ConvertFileUriToPath(uri); + + Assert.NotNull(result); + var expected = string.IsNullOrWhiteSpace(home) + ? Path.GetFullPath("/C:/~/Sources/project") + : Path.GetFullPath(Path.Combine(home, "Sources", "project")); + Assert.Equal(expected, result); + } + [Fact] public void ConvertFileUri_ResultIsFullPath() { From a9ab6945088abe7dd84258727c714c9d1dbcbaa7 Mon Sep 17 00:00:00 2001 From: Zdenek Srejber Date: Wed, 27 May 2026 11:49:29 +0200 Subject: [PATCH 2/5] feat: enhance drive-qualified home path normalization and add corresponding tests --- src/TALXIS.CLI.MCP/McpPathNormalizer.cs | 21 ++++++++-- .../MCP/McpPathNormalizerTests.cs | 42 ++++++++++++------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/TALXIS.CLI.MCP/McpPathNormalizer.cs b/src/TALXIS.CLI.MCP/McpPathNormalizer.cs index 636d201..6ff7ed7 100644 --- a/src/TALXIS.CLI.MCP/McpPathNormalizer.cs +++ b/src/TALXIS.CLI.MCP/McpPathNormalizer.cs @@ -56,6 +56,7 @@ internal static string ExpandHomeRelativePath(string path, bool allowFileUriLoca return segments.Length == 0 ? home : Path.Combine([home, .. segments]); } + // Matches drive-qualified home paths like C:/~/project and C:\~\project. private static string? TryGetDriveQualifiedHomeRemainder(string path) { if (!OperatingSystem.IsWindows()) @@ -65,14 +66,23 @@ internal static string ExpandHomeRelativePath(string path, bool allowFileUriLoca return null; if (path[2] == '~') - return path[3..]; + return path.Length == 3 + ? string.Empty + : IsDirectorySeparator(path[3]) + ? path[4..] + : null; if (path.Length >= 4 && IsDirectorySeparator(path[2]) && path[3] == '~') - return path[4..]; + return path.Length == 4 + ? string.Empty + : IsDirectorySeparator(path[4]) + ? path[4..] + : null; return null; } + // Matches file URI local paths like /C:/~/project after Uri.LocalPath. private static string? TryGetFileUriDriveQualifiedHomeRemainder(string path) { if (!OperatingSystem.IsWindows()) @@ -81,9 +91,14 @@ internal static string ExpandHomeRelativePath(string path, bool allowFileUriLoca if (path.Length < 5 || path[0] != '/' || !char.IsLetter(path[1]) || path[2] != ':' || !IsDirectorySeparator(path[3]) || path[4] != '~') return null; - return path[5..]; + return path.Length == 5 + ? string.Empty + : IsDirectorySeparator(path[5]) + ? path[5..] + : null; } + // Drops the leading slash from file URI local paths like /c:/project. private static string? TryNormalizeWindowsFileUriDrivePath(string path) { if (OperatingSystem.IsWindows() && path.Length >= 3 diff --git a/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs b/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs index 5ea19ee..d25cf2a 100644 --- a/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs +++ b/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs @@ -24,25 +24,39 @@ public void NormalizeOperationalPath_HomeRelativeInput_ResolvesAgainstUserProfil } [Theory] - [InlineData("C:~")] - [InlineData("C:/~")] - [InlineData("C:\\~")] - [InlineData("C:/~/Sources/project")] - [InlineData("C:\\~\\Sources\\project")] - [InlineData("c:/~")] - [InlineData("c:/~/Sources/project")] - [InlineData("c:\\~\\Sources\\project")] - [InlineData("/C:/~/Sources/project")] - [InlineData("/c:/~/Sources/project")] - public void NormalizeOperationalPath_DriveQualifiedHome_UsesUserProfile(string input) + [InlineData("C:~", null)] + [InlineData("C:/~", null)] + [InlineData("C:\\~", null)] + [InlineData("C:/~/Sources/project", "Sources/project")] + [InlineData("C:\\~\\Sources\\project", "Sources/project")] + [InlineData("c:/~", null)] + [InlineData("c:/~/Sources/project", "Sources/project")] + [InlineData("c:\\~\\Sources\\project", "Sources/project")] + [InlineData("/C:/~/Sources/project", "Sources/project")] + [InlineData("/c:/~/Sources/project", "Sources/project")] + public void NormalizeOperationalPath_DriveQualifiedHome_UsesUserProfile(string input, string? relativeToHome) { var home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile); var result = McpPathNormalizer.NormalizeOperationalPath(input); var expected = string.IsNullOrWhiteSpace(home) ? Path.GetFullPath(input) - : input.EndsWith("~") || input.EndsWith("~/") || input.EndsWith("~\\") - ? Path.GetFullPath(home) - : Path.GetFullPath(Path.Combine(home, "Sources", "project")); + : Path.GetFullPath(relativeToHome is null ? home : Path.Combine(home, relativeToHome)); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("C:~folder", false)] + [InlineData("C:/~folder", false)] + [InlineData("C:\\~folder", false)] + [InlineData("/C:/~folder", true)] + [InlineData("/c:/~folder", true)] + public void NormalizeOperationalPath_NonDelimitedDriveQualifiedTilde_RemainsFilesystemPath(string input, bool isFileUriLocalDrivePath) + { + var result = McpPathNormalizer.NormalizeOperationalPath(input); + var expected = OperatingSystem.IsWindows() && isFileUriLocalDrivePath + ? Path.GetFullPath(input[1..]) + : Path.GetFullPath(input); + Assert.Equal(expected, result); } From 56e16f6a537d181dcf7fb2cb7315aa79e0ffc80d Mon Sep 17 00:00:00 2001 From: Zdenek Srejber Date: Wed, 27 May 2026 14:24:41 +0200 Subject: [PATCH 3/5] Tighten MCP path normalization on Windows --- src/TALXIS.CLI.MCP/McpPathNormalizer.cs | 70 ++----------------- .../MCP/McpPathNormalizerTests.cs | 27 +++---- .../TALXIS.CLI.Tests/MCP/RootsServiceTests.cs | 9 ++- 3 files changed, 19 insertions(+), 87 deletions(-) diff --git a/src/TALXIS.CLI.MCP/McpPathNormalizer.cs b/src/TALXIS.CLI.MCP/McpPathNormalizer.cs index 6ff7ed7..ada0792 100644 --- a/src/TALXIS.CLI.MCP/McpPathNormalizer.cs +++ b/src/TALXIS.CLI.MCP/McpPathNormalizer.cs @@ -7,9 +7,9 @@ public static string NormalizeOperationalPath(string path, bool allowFileUriLoca if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Path must not be empty.", nameof(path)); - var expanded = ExpandDriveQualifiedHomePath(path); - if (expanded != null) - return Path.GetFullPath(expanded); + var normalized = TryNormalizeWindowsFileUriDrivePath(path); + if (normalized != null) + return Path.GetFullPath(normalized); return Path.GetFullPath(ExpandHomeRelativePath(path, allowFileUriLocalPathHome)); } @@ -26,22 +26,6 @@ internal static string ExpandHomeRelativePath(string path, bool allowFileUriLoca return TryExpandHomePath(path[suffixStart..]) ?? path; } - internal static string? ExpandDriveQualifiedHomePath(string path) - { - if (string.IsNullOrWhiteSpace(path)) - return null; - - var driveQualifiedHomeRemainder = TryGetDriveQualifiedHomeRemainder(path); - if (driveQualifiedHomeRemainder != null) - return TryExpandHomePath(driveQualifiedHomeRemainder); - - var fileUriDriveQualifiedHomeRemainder = TryGetFileUriDriveQualifiedHomeRemainder(path); - if (fileUriDriveQualifiedHomeRemainder != null) - return TryExpandHomePath(fileUriDriveQualifiedHomeRemainder); - - return TryNormalizeWindowsFileUriDrivePath(path); - } - private static string? TryExpandHomePath(string remainder) { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); @@ -56,49 +40,8 @@ internal static string ExpandHomeRelativePath(string path, bool allowFileUriLoca return segments.Length == 0 ? home : Path.Combine([home, .. segments]); } - // Matches drive-qualified home paths like C:/~/project and C:\~\project. - private static string? TryGetDriveQualifiedHomeRemainder(string path) - { - if (!OperatingSystem.IsWindows()) - return null; - - if (!IsDriveQualifiedPath(path)) - return null; - - if (path[2] == '~') - return path.Length == 3 - ? string.Empty - : IsDirectorySeparator(path[3]) - ? path[4..] - : null; - - if (path.Length >= 4 && IsDirectorySeparator(path[2]) && path[3] == '~') - return path.Length == 4 - ? string.Empty - : IsDirectorySeparator(path[4]) - ? path[4..] - : null; - - return null; - } - - // Matches file URI local paths like /C:/~/project after Uri.LocalPath. - private static string? TryGetFileUriDriveQualifiedHomeRemainder(string path) - { - if (!OperatingSystem.IsWindows()) - return null; - - if (path.Length < 5 || path[0] != '/' || !char.IsLetter(path[1]) || path[2] != ':' || !IsDirectorySeparator(path[3]) || path[4] != '~') - return null; - - return path.Length == 5 - ? string.Empty - : IsDirectorySeparator(path[5]) - ? path[5..] - : null; - } - // Drops the leading slash from file URI local paths like /c:/project. + // This is a mechanical Uri.LocalPath normalization, not a semantic rewrite. private static string? TryNormalizeWindowsFileUriDrivePath(string path) { if (OperatingSystem.IsWindows() && path.Length >= 3 @@ -110,11 +53,6 @@ internal static string ExpandHomeRelativePath(string path, bool allowFileUriLoca return null; } - private static bool IsDriveQualifiedPath(string path) - { - return path.Length >= 3 && char.IsLetter(path[0]) && path[1] == ':'; - } - private static int GetHomeRelativeSuffixStart(string path, bool allowFileUriLocalPathHome) { if (path == "~") diff --git a/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs b/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs index d25cf2a..c7f744e 100644 --- a/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs +++ b/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs @@ -24,24 +24,19 @@ public void NormalizeOperationalPath_HomeRelativeInput_ResolvesAgainstUserProfil } [Theory] - [InlineData("C:~", null)] - [InlineData("C:/~", null)] - [InlineData("C:\\~", null)] - [InlineData("C:/~/Sources/project", "Sources/project")] - [InlineData("C:\\~\\Sources\\project", "Sources/project")] - [InlineData("c:/~", null)] - [InlineData("c:/~/Sources/project", "Sources/project")] - [InlineData("c:\\~\\Sources\\project", "Sources/project")] - [InlineData("/C:/~/Sources/project", "Sources/project")] - [InlineData("/c:/~/Sources/project", "Sources/project")] - public void NormalizeOperationalPath_DriveQualifiedHome_UsesUserProfile(string input, string? relativeToHome) + [InlineData("C:~")] + [InlineData("C:/~")] + [InlineData("C:\\~")] + [InlineData("C:/~/Sources/project")] + [InlineData("C:\\~\\Sources\\project")] + [InlineData("c:/~")] + [InlineData("c:/~/Sources/project")] + [InlineData("c:\\~\\Sources\\project")] + public void NormalizeOperationalPath_DriveQualifiedTilde_RemainsFilesystemPath(string input) { - var home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile); var result = McpPathNormalizer.NormalizeOperationalPath(input); - var expected = string.IsNullOrWhiteSpace(home) - ? Path.GetFullPath(input) - : Path.GetFullPath(relativeToHome is null ? home : Path.Combine(home, relativeToHome)); - Assert.Equal(expected, result); + + Assert.Equal(Path.GetFullPath(input), result); } [Theory] diff --git a/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs b/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs index 2224718..7c5ae10 100644 --- a/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs +++ b/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs @@ -100,15 +100,14 @@ public void ConvertFileUri_HomeRelativePath_ResolvesAgainstUserProfile() [Theory] [InlineData("file:///C:/~/Sources/project")] [InlineData("file:///c:/~/Sources/project")] - public void ConvertFileUri_DriveQualifiedHome_UsesUserProfile(string uri) + public void ConvertFileUri_DriveQualifiedHome_RemainsFilesystemPath(string uri) { - var home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile); var result = RootsService.ConvertFileUriToPath(uri); Assert.NotNull(result); - var expected = string.IsNullOrWhiteSpace(home) - ? Path.GetFullPath("/C:/~/Sources/project") - : Path.GetFullPath(Path.Combine(home, "Sources", "project")); + var expected = OperatingSystem.IsWindows() + ? Path.GetFullPath(uri.Contains("/c:/", StringComparison.Ordinal) ? "c:/~/Sources/project" : "C:/~/Sources/project") + : Path.GetFullPath(uri.Contains("/c:/", StringComparison.Ordinal) ? "/c:/~/Sources/project" : "/C:/~/Sources/project"); Assert.Equal(expected, result); } From 256b1a8c5f17e488c42c1e91a377ad8c2c3dec7a Mon Sep 17 00:00:00 2001 From: Zdenek Srejber Date: Wed, 27 May 2026 14:41:12 +0200 Subject: [PATCH 4/5] Refine MCP path normalization tests --- tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs | 4 ++-- tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs b/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs index c7f744e..1cb7c2c 100644 --- a/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs +++ b/tests/TALXIS.CLI.Tests/MCP/McpPathNormalizerTests.cs @@ -24,7 +24,6 @@ public void NormalizeOperationalPath_HomeRelativeInput_ResolvesAgainstUserProfil } [Theory] - [InlineData("C:~")] [InlineData("C:/~")] [InlineData("C:\\~")] [InlineData("C:/~/Sources/project")] @@ -40,9 +39,10 @@ public void NormalizeOperationalPath_DriveQualifiedTilde_RemainsFilesystemPath(s } [Theory] - [InlineData("C:~folder", false)] [InlineData("C:/~folder", false)] [InlineData("C:\\~folder", false)] + [InlineData("c:/~folder", false)] + [InlineData("c:\\~folder", false)] [InlineData("/C:/~folder", true)] [InlineData("/c:/~folder", true)] public void NormalizeOperationalPath_NonDelimitedDriveQualifiedTilde_RemainsFilesystemPath(string input, bool isFileUriLocalDrivePath) diff --git a/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs b/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs index 7c5ae10..09558e1 100644 --- a/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs +++ b/tests/TALXIS.CLI.Tests/MCP/RootsServiceTests.cs @@ -98,17 +98,15 @@ public void ConvertFileUri_HomeRelativePath_ResolvesAgainstUserProfile() } [Theory] - [InlineData("file:///C:/~/Sources/project")] - [InlineData("file:///c:/~/Sources/project")] - public void ConvertFileUri_DriveQualifiedHome_RemainsFilesystemPath(string uri) + [InlineData("file:///C:/~/Sources/project", "C:/~/Sources/project", "/C:/~/Sources/project")] + [InlineData("file:///c:/~/Sources/project", "c:/~/Sources/project", "/c:/~/Sources/project")] + public void ConvertFileUri_DriveQualifiedHome_RemainsFilesystemPath(string uri, string windowsPath, string nonWindowsPath) { var result = RootsService.ConvertFileUriToPath(uri); Assert.NotNull(result); - var expected = OperatingSystem.IsWindows() - ? Path.GetFullPath(uri.Contains("/c:/", StringComparison.Ordinal) ? "c:/~/Sources/project" : "C:/~/Sources/project") - : Path.GetFullPath(uri.Contains("/c:/", StringComparison.Ordinal) ? "/c:/~/Sources/project" : "/C:/~/Sources/project"); - Assert.Equal(expected, result); + var expectedInput = OperatingSystem.IsWindows() ? windowsPath : nonWindowsPath; + Assert.Equal(Path.GetFullPath(expectedInput), result); } [Fact] From 7a103c33e87966f762ecffad5084b63b71b59a72 Mon Sep 17 00:00:00 2001 From: Zdenek Srejber Date: Wed, 27 May 2026 15:29:35 +0200 Subject: [PATCH 5/5] moved normalization by prioritizing Windows file URI handling --- src/TALXIS.CLI.MCP/McpPathNormalizer.cs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/TALXIS.CLI.MCP/McpPathNormalizer.cs b/src/TALXIS.CLI.MCP/McpPathNormalizer.cs index ada0792..a557f2e 100644 --- a/src/TALXIS.CLI.MCP/McpPathNormalizer.cs +++ b/src/TALXIS.CLI.MCP/McpPathNormalizer.cs @@ -7,10 +7,6 @@ public static string NormalizeOperationalPath(string path, bool allowFileUriLoca if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Path must not be empty.", nameof(path)); - var normalized = TryNormalizeWindowsFileUriDrivePath(path); - if (normalized != null) - return Path.GetFullPath(normalized); - return Path.GetFullPath(ExpandHomeRelativePath(path, allowFileUriLocalPathHome)); } @@ -19,24 +15,24 @@ internal static string ExpandHomeRelativePath(string path, bool allowFileUriLoca if (string.IsNullOrWhiteSpace(path)) return path; + // Normalize Windows file URI local drive paths like "/c:/project" early. + var normalizedPath = TryNormalizeWindowsFileUriDrivePath(path); + if (normalizedPath != null) + return normalizedPath; + var suffixStart = GetHomeRelativeSuffixStart(path, allowFileUriLocalPathHome); if (suffixStart < 0) return path; - return TryExpandHomePath(path[suffixStart..]) ?? path; - } - - private static string? TryExpandHomePath(string remainder) - { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); if (string.IsNullOrWhiteSpace(home)) - return null; + return path; - var trimmedRemainder = remainder.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, '\\', '/'); - if (string.IsNullOrEmpty(trimmedRemainder)) + var remainder = path[suffixStart..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, '\\', '/'); + if (string.IsNullOrEmpty(remainder)) return home; - var segments = trimmedRemainder.Split(['\\', '/'], StringSplitOptions.RemoveEmptyEntries); + var segments = remainder.Split(['\\', '/'], StringSplitOptions.RemoveEmptyEntries); return segments.Length == 0 ? home : Path.Combine([home, .. segments]); }