From bbfa8282b96b1cf637ea8e9f24c656f33b8bd547 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:20:09 +0000 Subject: [PATCH 1/4] Initial plan From 476f2281dffcc02b0c07f64df2cb474d744ae17e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:25:05 +0000 Subject: [PATCH 2/4] feat: add graphrag corpus visibility diagnostics Co-authored-by: sharpninja <16146732+sharpninja@users.noreply.github.com> --- src/McpServer.Client/Models/ContextModels.cs | 33 +++++++++++++++---- .../Models/GraphRagModels.cs | 7 ++++ .../ExternalCommandGraphRagBackendAdapter.cs | 4 ++- .../Services/GraphRagService.cs | 18 ++++++++-- .../Controllers/GraphRagControllerTests.cs | 20 +++++++++++ .../Services/GraphRagServiceTests.cs | 19 +++++++++++ 6 files changed, 92 insertions(+), 9 deletions(-) diff --git a/src/McpServer.Client/Models/ContextModels.cs b/src/McpServer.Client/Models/ContextModels.cs index fea20f62..b1e5bb18 100644 --- a/src/McpServer.Client/Models/ContextModels.cs +++ b/src/McpServer.Client/Models/ContextModels.cs @@ -271,9 +271,24 @@ public sealed class GraphRagStatusResult [JsonPropertyName("lastIndexedDocumentCount")] public int? LastIndexedDocumentCount { get; set; } - [JsonPropertyName("backend")] - public string Backend { get; set; } = string.Empty; -} + [JsonPropertyName("backend")] + public string Backend { get; set; } = string.Empty; + + [JsonPropertyName("indexCorpus")] + public string IndexCorpus { get; set; } = string.Empty; + + [JsonPropertyName("queryCorpus")] + public string QueryCorpus { get; set; } = string.Empty; + + [JsonPropertyName("inputPath")] + public string InputPath { get; set; } = string.Empty; + + [JsonPropertyName("inputDocumentCount")] + public int InputDocumentCount { get; set; } + + [JsonPropertyName("visibilityNote")] + public string? VisibilityNote { get; set; } +} /// GraphRAG citation entry. public sealed class GraphRagCitation @@ -327,8 +342,14 @@ public sealed class GraphRagQueryResult [JsonPropertyName("failureCode")] public string? FailureCode { get; set; } - [JsonPropertyName("backend")] - public string Backend { get; set; } = string.Empty; -} + [JsonPropertyName("backend")] + public string Backend { get; set; } = string.Empty; + + [JsonPropertyName("queryCorpus")] + public string QueryCorpus { get; set; } = string.Empty; + + [JsonPropertyName("visibilityNote")] + public string? VisibilityNote { get; set; } +} #pragma warning restore CS1591 diff --git a/src/McpServer.GraphRag/Models/GraphRagModels.cs b/src/McpServer.GraphRag/Models/GraphRagModels.cs index f6d1d292..0b60eb99 100644 --- a/src/McpServer.GraphRag/Models/GraphRagModels.cs +++ b/src/McpServer.GraphRag/Models/GraphRagModels.cs @@ -40,6 +40,11 @@ public sealed class GraphRagStatusResponse public long? LastIndexDurationMs { get; set; } public int? LastIndexedDocumentCount { get; set; } public string Backend { get; set; } = "internal-fallback"; + public string IndexCorpus { get; set; } = "graphrag-input"; + public string QueryCorpus { get; set; } = "context-search"; + public string InputPath { get; set; } = string.Empty; + public int InputDocumentCount { get; set; } + public string? VisibilityNote { get; set; } } /// Citation payload from GraphRAG query responses. @@ -66,6 +71,8 @@ public sealed class GraphRagQueryResponse public string? FallbackReason { get; set; } public string? FailureCode { get; set; } public string Backend { get; set; } = "internal-fallback"; + public string QueryCorpus { get; set; } = "context-search"; + public string? VisibilityNote { get; set; } } #pragma warning restore CS1591 diff --git a/src/McpServer.GraphRag/Services/ExternalCommandGraphRagBackendAdapter.cs b/src/McpServer.GraphRag/Services/ExternalCommandGraphRagBackendAdapter.cs index fc08d3ca..c03ae8f2 100644 --- a/src/McpServer.GraphRag/Services/ExternalCommandGraphRagBackendAdapter.cs +++ b/src/McpServer.GraphRag/Services/ExternalCommandGraphRagBackendAdapter.cs @@ -85,7 +85,9 @@ public async Task IndexAsync(GraphRagBackendExecutio FallbackUsed = false, FallbackReason = null, FailureCode = null, - Backend = AdapterName + Backend = AdapterName, + QueryCorpus = "graphrag-backend", + VisibilityNote = null }; } catch (Exception ex) when (ex is not OperationCanceledException) diff --git a/src/McpServer.GraphRag/Services/GraphRagService.cs b/src/McpServer.GraphRag/Services/GraphRagService.cs index 61d5e38e..8a22331f 100644 --- a/src/McpServer.GraphRag/Services/GraphRagService.cs +++ b/src/McpServer.GraphRag/Services/GraphRagService.cs @@ -50,6 +50,7 @@ public async Task GetStatusAsync(CancellationToken cance { var workspacePath = ResolveWorkspacePath(); var graphRoot = ResolveGraphRoot(workspacePath); + var inputPath = Path.Combine(graphRoot, "input"); var persisted = await TryReadStatusAsync(graphRoot, cancellationToken).ConfigureAwait(false); var backend = SelectBackend(); var initialized = HasInitializedStructure(graphRoot); @@ -57,6 +58,10 @@ public async Task GetStatusAsync(CancellationToken cance var isIndexedByArtifact = IsReadyArtifactPresent(graphRoot); var isIndexed = persisted?.IsIndexed == true && isIndexedByArtifact; var backendAvailabilityError = GetBackendAvailabilityError(backend); + var inputDocumentCount = Directory.Exists(inputPath) + ? Directory.EnumerateFiles(inputPath, "*", SearchOption.AllDirectories).Count() + : 0; + var isInternalFallback = string.Equals(backend.AdapterName, "internal-fallback", StringComparison.OrdinalIgnoreCase); return new GraphRagStatusResponse { @@ -75,7 +80,14 @@ public async Task GetStatusAsync(CancellationToken cance ArtifactVersion = persisted?.ArtifactVersion ?? _options.ArtifactVersion, LastIndexDurationMs = persisted?.LastIndexDurationMs, LastIndexedDocumentCount = persisted?.LastIndexedDocumentCount, - Backend = backend.AdapterName + Backend = backend.AdapterName, + IndexCorpus = "graphrag-input", + QueryCorpus = isInternalFallback ? "context-search" : "graphrag-backend", + InputPath = inputPath, + InputDocumentCount = inputDocumentCount, + VisibilityNote = isInternalFallback + ? "internal-fallback indexes files under GraphRAG input but query results come from context-search." + : null }; } @@ -268,7 +280,9 @@ public async Task QueryAsync(GraphRagQueryRequest request FallbackUsed = fallbackUsed, FallbackReason = fallbackReason, FailureCode = fallbackUsed ? "query_fallback" : null, - Backend = SelectBackend().AdapterName + Backend = SelectBackend().AdapterName, + QueryCorpus = "context-search", + VisibilityNote = "Fallback query uses context-search chunks; GraphRAG input visibility depends on ingestion into context-search." }; } diff --git a/tests/McpServer.Support.Mcp.IntegrationTests/Controllers/GraphRagControllerTests.cs b/tests/McpServer.Support.Mcp.IntegrationTests/Controllers/GraphRagControllerTests.cs index fd0d61a8..c2fc2315 100644 --- a/tests/McpServer.Support.Mcp.IntegrationTests/Controllers/GraphRagControllerTests.cs +++ b/tests/McpServer.Support.Mcp.IntegrationTests/Controllers/GraphRagControllerTests.cs @@ -25,6 +25,10 @@ public async Task Status_ReturnsOk() using var doc = JsonDocument.Parse(json); Assert.True(doc.RootElement.TryGetProperty("enabled", out _)); Assert.True(doc.RootElement.TryGetProperty("graphRoot", out _)); + Assert.True(doc.RootElement.TryGetProperty("indexCorpus", out _)); + Assert.True(doc.RootElement.TryGetProperty("queryCorpus", out _)); + Assert.True(doc.RootElement.TryGetProperty("inputPath", out _)); + Assert.True(doc.RootElement.TryGetProperty("inputDocumentCount", out _)); } [Fact] @@ -57,4 +61,20 @@ public async Task Query_WithInvalidMaxChunks_ReturnsBadRequest() Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } + + [Fact] + public async Task Query_ReturnsCorpusDiagnostics() + { + var response = await _client.PostAsJsonAsync(new Uri("/mcpserver/graphrag/query", UriKind.Relative), new + { + query = "auth", + maxChunks = 5 + }).ConfigureAwait(true); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(true); + using var doc = JsonDocument.Parse(json); + Assert.True(doc.RootElement.TryGetProperty("queryCorpus", out _)); + Assert.True(doc.RootElement.TryGetProperty("visibilityNote", out _)); + } } diff --git a/tests/McpServer.Support.Mcp.Tests/Services/GraphRagServiceTests.cs b/tests/McpServer.Support.Mcp.Tests/Services/GraphRagServiceTests.cs index 248b8855..6501feb8 100644 --- a/tests/McpServer.Support.Mcp.Tests/Services/GraphRagServiceTests.cs +++ b/tests/McpServer.Support.Mcp.Tests/Services/GraphRagServiceTests.cs @@ -62,6 +62,25 @@ public async Task QueryAsync_WhenDisabled_ReturnsFallbackReason() Assert.True(response.FallbackUsed); Assert.Equal("graphrag_disabled", response.FallbackReason); + Assert.Equal("context-search", response.QueryCorpus); + } + + [Fact] + public async Task Status_InternalFallback_ReportsCorpusAndInputDiagnostics() + { + var sut = CreateSut(enabled: true); + var initialized = await sut.InitializeAsync().ConfigureAwait(true); + var localDocPath = Path.Combine(initialized.GraphRoot, "input", "docs", "prg", "Commodore_64_Programmers_Reference_Guide.txt"); + Directory.CreateDirectory(Path.GetDirectoryName(localDocPath)!); + await File.WriteAllTextAsync(localDocPath, "Video Bank Selection").ConfigureAwait(true); + + var status = await sut.GetStatusAsync().ConfigureAwait(true); + + Assert.Equal("graphrag-input", status.IndexCorpus); + Assert.Equal("context-search", status.QueryCorpus); + Assert.Equal(Path.Combine(status.GraphRoot, "input"), status.InputPath); + Assert.Equal(1, status.InputDocumentCount); + Assert.Contains("internal-fallback", status.VisibilityNote, StringComparison.OrdinalIgnoreCase); } [Fact] From accca158811a136a08a6c4ba8546c9ba8ee768f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:06:16 +0000 Subject: [PATCH 3/4] fix: unblock ci config validation and docs lint Co-authored-by: sharpninja <16146732+sharpninja@users.noreply.github.com> --- docs/USER-GUIDE.md | 3 +- scripts/Validate-McpConfig.ps1 | 223 +++++++++++++++++++++++++++------ 2 files changed, 184 insertions(+), 42 deletions(-) diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 5d703c83..86c52391 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -172,7 +172,7 @@ The sample host: - supports configurable response verbosity through `SampleHost:Verbosity` or `MCP_AGENT_VERBOSITY` - supports configurable OpenAI client network timeout and retry count through `SampleHost:Model:NetworkTimeoutSeconds`, `SampleHost:Model:MaxRetries`, `OPENAI_NETWORK_TIMEOUT_SECONDS`, and `OPENAI_MAX_RETRIES` - lets you switch verbosity live with `/v 1`, `/v 2`, or `/v 3` -- routes lines prefixed with `! ` directly into the hosted local PowerShell session while leaving other lines as normal chat prompts +- routes lines prefixed with `!` plus a space directly into the hosted local PowerShell session while leaving other lines as normal chat prompts - logs chat turns through the hosted session-log workflow - exposes repository tools, `mcp_desktop_launch` for authenticated local program execution through MCP Server, `mcp_powershell_session_*` for model-facing stateful PowerShell execution, and `IMcpHostedAgent.PowerShellSessions` for host-facing direct PowerShell execution @@ -556,4 +556,3 @@ Invoke-RestMethod -Method Post -Uri "http://localhost:7147/mcpserver/workspace" - FAQ: `docs/FAQ.md` - Context docs: `docs/context/` - Tunnel runbooks: `docs/Operations/` - diff --git a/scripts/Validate-McpConfig.ps1 b/scripts/Validate-McpConfig.ps1 index 068df8b4..d07467b9 100644 --- a/scripts/Validate-McpConfig.ps1 +++ b/scripts/Validate-McpConfig.ps1 @@ -2,35 +2,178 @@ .SYNOPSIS Validates MCP appsettings instance configuration. #> -[CmdletBinding()] -param( - [string]$ConfigPath = "src/McpServer.Support.Mcp/appsettings.json" -) - -$ErrorActionPreference = "Stop" - -if (-not (Test-Path $ConfigPath)) { - throw "Config file not found: $ConfigPath" -} - -$json = Get-Content -Raw -Path $ConfigPath | ConvertFrom-Json -if (-not $json.Mcp) { - throw "Missing 'Mcp' section." -} - -$instances = $json.Mcp.Instances -if (-not $instances) { - Write-Host "No Mcp:Instances configured. Validation passed." - exit 0 -} - -$ports = @{} -$instances.PSObject.Properties | ForEach-Object { - $name = $_.Name - $instance = $_.Value - - if (-not $instance.RepoRoot) { - throw "Instance '$name' missing RepoRoot." +[CmdletBinding()] +param( + [string]$ConfigPath = "" +) + +$ErrorActionPreference = "Stop" + +function ConvertFrom-YamlScalar { + param( + [string]$Value + ) + + $trimmed = $Value.Trim() + if (($trimmed.StartsWith("'") -and $trimmed.EndsWith("'")) -or ($trimmed.StartsWith('"') -and $trimmed.EndsWith('"'))) { + return $trimmed.Substring(1, $trimmed.Length - 2) + } + + return $trimmed +} + +function Get-McpInstancesFromYaml { + param( + [string]$Path + ) + + $lines = Get-Content -Path $Path + $hasMcp = $false + $instances = [ordered]@{} + $inInstances = $false + $currentInstance = $null + $inTodoStorage = $false + + foreach ($rawLine in $lines) { + $line = $rawLine.TrimEnd() + if ([string]::IsNullOrWhiteSpace($line) -or $line.TrimStart().StartsWith('#')) { + continue + } + + if ($line -match '^Mcp:\s*$') { + $hasMcp = $true + continue + } + + if (-not $hasMcp) { + continue + } + + if ($line -match '^ Instances:\s*$') { + $inInstances = $true + $currentInstance = $null + $inTodoStorage = $false + continue + } + + if (-not $inInstances) { + continue + } + + if ($line -match '^ [A-Za-z0-9_-]+:\s*$') { + break + } + + if ($line -match '^ ([^:\s][^:]*):\s*$') { + $currentInstance = $Matches[1] + $instances[$currentInstance] = [ordered]@{ + RepoRoot = $null + Port = $null + TodoStorage = [ordered]@{ + Provider = $null + SqliteDataSource = $null + } + } + $inTodoStorage = $false + continue + } + + if ($null -eq $currentInstance) { + continue + } + + if ($line -match '^ TodoStorage:\s*$') { + $inTodoStorage = $true + continue + } + + if ($line -match '^ [A-Za-z0-9_-]+:\s*') { + $inTodoStorage = $false + } + + if ($line -match '^ RepoRoot:\s*(.+)$') { + $instances[$currentInstance].RepoRoot = ConvertFrom-YamlScalar $Matches[1] + continue + } + + if ($line -match '^ Port:\s*(.+)$') { + $instances[$currentInstance].Port = ConvertFrom-YamlScalar $Matches[1] + continue + } + + if ($inTodoStorage -and $line -match '^ Provider:\s*(.+)$') { + $instances[$currentInstance].TodoStorage.Provider = ConvertFrom-YamlScalar $Matches[1] + continue + } + + if ($inTodoStorage -and $line -match '^ SqliteDataSource:\s*(.+)$') { + $instances[$currentInstance].TodoStorage.SqliteDataSource = ConvertFrom-YamlScalar $Matches[1] + continue + } + } + + return @{ + HasMcp = $hasMcp + Instances = $instances + } +} + +if ([string]::IsNullOrWhiteSpace($ConfigPath)) { + $candidatePaths = @( + "src/McpServer.Support.Mcp/appsettings.yaml", + "src/McpServer.Support.Mcp/appsettings.yml", + "src/McpServer.Support.Mcp/appsettings.json" + ) + $ConfigPath = $candidatePaths | Where-Object { Test-Path $_ } | Select-Object -First 1 +} + +if (-not (Test-Path $ConfigPath)) { + throw "Config file not found: $ConfigPath" +} + +$extension = [System.IO.Path]::GetExtension($ConfigPath) +$config = switch ($extension.ToLowerInvariant()) { + ".yaml" { Get-McpInstancesFromYaml -Path $ConfigPath } + ".yml" { Get-McpInstancesFromYaml -Path $ConfigPath } + ".json" { + $json = Get-Content -Raw -Path $ConfigPath | ConvertFrom-Json + @{ + HasMcp = $null -ne $json.Mcp + Instances = $json.Mcp.Instances + } + } + default { throw "Unsupported config format '$extension' for '$ConfigPath'." } +} + +if (-not $config.HasMcp) { + throw "Missing 'Mcp' section." +} + +$instances = $config.Instances +if (-not $instances) { + Write-Host "No Mcp:Instances configured. Validation passed." + exit 0 +} + +$instanceEntries = if ($instances -is [System.Collections.IDictionary]) { + $instances.GetEnumerator() | Sort-Object Name +} +else { + $instances.PSObject.Properties | ForEach-Object { + [pscustomobject]@{ + Name = $_.Name + Value = $_.Value + } + } +} + +$ports = @{} +$instanceEntries | ForEach-Object { + $name = $_.Name + $instance = $_.Value + + if (-not $instance.RepoRoot) { + throw "Instance '$name' missing RepoRoot." } $resolvedRoot = [System.IO.Path]::GetFullPath([string]$instance.RepoRoot) if (-not (Test-Path -Path $resolvedRoot -PathType Container)) { @@ -58,14 +201,14 @@ $instances.PSObject.Properties | ForEach-Object { if ($provider -eq "sqlite") { $sqliteDataSource = "" - if ($instance.TodoStorage -and $instance.TodoStorage.SqliteDataSource) { - $sqliteDataSource = [string]$instance.TodoStorage.SqliteDataSource - } - if ([string]::IsNullOrWhiteSpace($sqliteDataSource)) { - throw "Instance '$name' provider sqlite requires TodoStorage.SqliteDataSource." - } - } -} - -$instanceCount = @($instances.PSObject.Properties).Count -Write-Host "MCP config validation passed for $instanceCount instances." + if ($instance.TodoStorage -and $instance.TodoStorage.SqliteDataSource) { + $sqliteDataSource = [string]$instance.TodoStorage.SqliteDataSource + } + if ([string]::IsNullOrWhiteSpace($sqliteDataSource)) { + throw "Instance '$name' provider sqlite requires TodoStorage.SqliteDataSource." + } + } +} + +$instanceCount = @($instanceEntries).Count +Write-Host "MCP config validation passed for $instanceCount instances." From baeb9979c6fc7699bfd6a6591b0e8fc28302ee1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:10:14 +0000 Subject: [PATCH 4/4] fix: harden ci validation script parsing Co-authored-by: sharpninja <16146732+sharpninja@users.noreply.github.com> --- scripts/Validate-McpConfig.ps1 | 90 ++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 16 deletions(-) diff --git a/scripts/Validate-McpConfig.ps1 b/scripts/Validate-McpConfig.ps1 index d07467b9..e36a20da 100644 --- a/scripts/Validate-McpConfig.ps1 +++ b/scripts/Validate-McpConfig.ps1 @@ -8,14 +8,26 @@ param( ) $ErrorActionPreference = "Stop" +$yamlKeyPattern = '[A-Za-z0-9_][A-Za-z0-9_\-]*' function ConvertFrom-YamlScalar { + <# + .SYNOPSIS + Normalizes a simple YAML scalar value for validation. + + .DESCRIPTION + Trims surrounding whitespace and removes matching single- or double-quote delimiters + when both ends use the same quote character. Mismatched quote pairs are left unchanged + so malformed values are not silently rewritten during validation. + #> param( [string]$Value ) $trimmed = $Value.Trim() - if (($trimmed.StartsWith("'") -and $trimmed.EndsWith("'")) -or ($trimmed.StartsWith('"') -and $trimmed.EndsWith('"'))) { + if ($trimmed.Length -ge 2 -and ( + ($trimmed[0] -eq "'" -and $trimmed[$trimmed.Length - 1] -eq "'") -or + ($trimmed[0] -eq '"' -and $trimmed[$trimmed.Length - 1] -eq '"'))) { return $trimmed.Substring(1, $trimmed.Length - 2) } @@ -23,6 +35,16 @@ function ConvertFrom-YamlScalar { } function Get-McpInstancesFromYaml { + <# + .SYNOPSIS + Extracts the Mcp:Instances block from the repository YAML settings file. + + .DESCRIPTION + Parses the checked-in appsettings YAML using the repository's current indentation pattern + so the validation script can run in CI without depending on an external YAML module. + The return value includes a HasMcp flag and an ordered dictionary of instance settings + containing RepoRoot, Port, and TodoStorage fields needed by this validator. + #> param( [string]$Path ) @@ -60,11 +82,12 @@ function Get-McpInstancesFromYaml { continue } - if ($line -match '^ [A-Za-z0-9_-]+:\s*$') { + if ($line -match "^ ${yamlKeyPattern}:\s*$") { + # A sibling key under Mcp means the Instances block has ended. break } - if ($line -match '^ ([^:\s][^:]*):\s*$') { + if ($line -match "^ (${yamlKeyPattern}):\s*$") { $currentInstance = $Matches[1] $instances[$currentInstance] = [ordered]@{ RepoRoot = $null @@ -87,7 +110,7 @@ function Get-McpInstancesFromYaml { continue } - if ($line -match '^ [A-Za-z0-9_-]+:\s*') { + if ($line -match '^ (RepoRoot|Port):\s*') { $inTodoStorage = $false } @@ -118,6 +141,51 @@ function Get-McpInstancesFromYaml { } } +function ConvertTo-McpInstanceMap { + <# + .SYNOPSIS + Normalizes parsed instance settings into a consistent ordered dictionary. + + .DESCRIPTION + Converts either JSON-derived PSCustomObject instances or the YAML parser output into + the same RepoRoot/Port/TodoStorage shape so the validation logic can iterate a single + data structure regardless of the source file format. + #> + param( + [object]$Instances + ) + + $instanceMap = [ordered]@{} + if ($null -eq $Instances) { + return $instanceMap + } + + $entries = if ($Instances -is [System.Collections.IDictionary]) { + $Instances.GetEnumerator() | Sort-Object Name + } + else { + $Instances.PSObject.Properties | ForEach-Object { + [pscustomobject]@{ + Name = $_.Name + Value = $_.Value + } + } | Sort-Object Name + } + + foreach ($entry in $entries) { + $instanceMap[$entry.Name] = [ordered]@{ + RepoRoot = $entry.Value.RepoRoot + Port = $entry.Value.Port + TodoStorage = [ordered]@{ + Provider = $entry.Value.TodoStorage.Provider + SqliteDataSource = $entry.Value.TodoStorage.SqliteDataSource + } + } + } + + return $instanceMap +} + if ([string]::IsNullOrWhiteSpace($ConfigPath)) { $candidatePaths = @( "src/McpServer.Support.Mcp/appsettings.yaml", @@ -139,7 +207,7 @@ $config = switch ($extension.ToLowerInvariant()) { $json = Get-Content -Raw -Path $ConfigPath | ConvertFrom-Json @{ HasMcp = $null -ne $json.Mcp - Instances = $json.Mcp.Instances + Instances = ConvertTo-McpInstanceMap -Instances $json.Mcp.Instances } } default { throw "Unsupported config format '$extension' for '$ConfigPath'." } @@ -155,17 +223,7 @@ if (-not $instances) { exit 0 } -$instanceEntries = if ($instances -is [System.Collections.IDictionary]) { - $instances.GetEnumerator() | Sort-Object Name -} -else { - $instances.PSObject.Properties | ForEach-Object { - [pscustomobject]@{ - Name = $_.Name - Value = $_.Value - } - } -} +$instanceEntries = $instances.GetEnumerator() | Sort-Object Name $ports = @{} $instanceEntries | ForEach-Object {