Skip to content

Commit 5bbd9e9

Browse files
authored
Merge pull request #29 from sharpninja/copilot/diagnose-local-text-visibility-issue
Add GraphRAG corpus visibility diagnostics for internal-fallback indexing/query mismatch
2 parents e00cc33 + 4f87933 commit 5bbd9e9

8 files changed

Lines changed: 334 additions & 51 deletions

File tree

docs/USER-GUIDE.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ The sample host:
172172
- supports configurable response verbosity through `SampleHost:Verbosity` or `MCP_AGENT_VERBOSITY`
173173
- supports configurable OpenAI client network timeout and retry count through `SampleHost:Model:NetworkTimeoutSeconds`, `SampleHost:Model:MaxRetries`, `OPENAI_NETWORK_TIMEOUT_SECONDS`, and `OPENAI_MAX_RETRIES`
174174
- lets you switch verbosity live with `/v 1`, `/v 2`, or `/v 3`
175-
- routes lines prefixed with `! ` directly into the hosted local PowerShell session while leaving other lines as normal chat prompts
175+
- routes lines prefixed with `!` plus a space directly into the hosted local PowerShell session while leaving other lines as normal chat prompts
176176
- logs chat turns through the hosted session-log workflow
177177
- 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
178178

@@ -556,4 +556,3 @@ Invoke-RestMethod -Method Post -Uri "http://localhost:7147/mcpserver/workspace"
556556
- FAQ: `docs/FAQ.md`
557557
- Context docs: `docs/context/`
558558
- Tunnel runbooks: `docs/Operations/`
559-

scripts/Validate-McpConfig.ps1

Lines changed: 241 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,236 @@
22
.SYNOPSIS
33
Validates MCP appsettings instance configuration.
44
#>
5-
[CmdletBinding()]
6-
param(
7-
[string]$ConfigPath = "src/McpServer.Support.Mcp/appsettings.json"
8-
)
9-
10-
$ErrorActionPreference = "Stop"
11-
12-
if (-not (Test-Path $ConfigPath)) {
13-
throw "Config file not found: $ConfigPath"
14-
}
15-
16-
$json = Get-Content -Raw -Path $ConfigPath | ConvertFrom-Json
17-
if (-not $json.Mcp) {
18-
throw "Missing 'Mcp' section."
19-
}
20-
21-
$instances = $json.Mcp.Instances
22-
if (-not $instances) {
23-
Write-Host "No Mcp:Instances configured. Validation passed."
24-
exit 0
25-
}
26-
27-
$ports = @{}
28-
$instances.PSObject.Properties | ForEach-Object {
29-
$name = $_.Name
30-
$instance = $_.Value
31-
32-
if (-not $instance.RepoRoot) {
33-
throw "Instance '$name' missing RepoRoot."
5+
[CmdletBinding()]
6+
param(
7+
[string]$ConfigPath = ""
8+
)
9+
10+
$ErrorActionPreference = "Stop"
11+
$yamlKeyPattern = '[A-Za-z0-9_][A-Za-z0-9_\-]*'
12+
13+
function ConvertFrom-YamlScalar {
14+
<#
15+
.SYNOPSIS
16+
Normalizes a simple YAML scalar value for validation.
17+
18+
.DESCRIPTION
19+
Trims surrounding whitespace and removes matching single- or double-quote delimiters
20+
when both ends use the same quote character. Mismatched quote pairs are left unchanged
21+
so malformed values are not silently rewritten during validation.
22+
#>
23+
param(
24+
[string]$Value
25+
)
26+
27+
$trimmed = $Value.Trim()
28+
if ($trimmed.Length -ge 2 -and (
29+
($trimmed[0] -eq "'" -and $trimmed[$trimmed.Length - 1] -eq "'") -or
30+
($trimmed[0] -eq '"' -and $trimmed[$trimmed.Length - 1] -eq '"'))) {
31+
return $trimmed.Substring(1, $trimmed.Length - 2)
32+
}
33+
34+
return $trimmed
35+
}
36+
37+
function Get-McpInstancesFromYaml {
38+
<#
39+
.SYNOPSIS
40+
Extracts the Mcp:Instances block from the repository YAML settings file.
41+
42+
.DESCRIPTION
43+
Parses the checked-in appsettings YAML using the repository's current indentation pattern
44+
so the validation script can run in CI without depending on an external YAML module.
45+
The return value includes a HasMcp flag and an ordered dictionary of instance settings
46+
containing RepoRoot, Port, and TodoStorage fields needed by this validator.
47+
#>
48+
param(
49+
[string]$Path
50+
)
51+
52+
$lines = Get-Content -Path $Path
53+
$hasMcp = $false
54+
$instances = [ordered]@{}
55+
$inInstances = $false
56+
$currentInstance = $null
57+
$inTodoStorage = $false
58+
59+
foreach ($rawLine in $lines) {
60+
$line = $rawLine.TrimEnd()
61+
if ([string]::IsNullOrWhiteSpace($line) -or $line.TrimStart().StartsWith('#')) {
62+
continue
63+
}
64+
65+
if ($line -match '^Mcp:\s*$') {
66+
$hasMcp = $true
67+
continue
68+
}
69+
70+
if (-not $hasMcp) {
71+
continue
72+
}
73+
74+
if ($line -match '^ Instances:\s*$') {
75+
$inInstances = $true
76+
$currentInstance = $null
77+
$inTodoStorage = $false
78+
continue
79+
}
80+
81+
if (-not $inInstances) {
82+
continue
83+
}
84+
85+
if ($line -match "^ ${yamlKeyPattern}:\s*$") {
86+
# A sibling key under Mcp means the Instances block has ended.
87+
break
88+
}
89+
90+
if ($line -match "^ (${yamlKeyPattern}):\s*$") {
91+
$currentInstance = $Matches[1]
92+
$instances[$currentInstance] = [ordered]@{
93+
RepoRoot = $null
94+
Port = $null
95+
TodoStorage = [ordered]@{
96+
Provider = $null
97+
SqliteDataSource = $null
98+
}
99+
}
100+
$inTodoStorage = $false
101+
continue
102+
}
103+
104+
if ($null -eq $currentInstance) {
105+
continue
106+
}
107+
108+
if ($line -match '^ TodoStorage:\s*$') {
109+
$inTodoStorage = $true
110+
continue
111+
}
112+
113+
if ($line -match '^ (RepoRoot|Port):\s*') {
114+
$inTodoStorage = $false
115+
}
116+
117+
if ($line -match '^ RepoRoot:\s*(.+)$') {
118+
$instances[$currentInstance].RepoRoot = ConvertFrom-YamlScalar $Matches[1]
119+
continue
120+
}
121+
122+
if ($line -match '^ Port:\s*(.+)$') {
123+
$instances[$currentInstance].Port = ConvertFrom-YamlScalar $Matches[1]
124+
continue
125+
}
126+
127+
if ($inTodoStorage -and $line -match '^ Provider:\s*(.+)$') {
128+
$instances[$currentInstance].TodoStorage.Provider = ConvertFrom-YamlScalar $Matches[1]
129+
continue
130+
}
131+
132+
if ($inTodoStorage -and $line -match '^ SqliteDataSource:\s*(.+)$') {
133+
$instances[$currentInstance].TodoStorage.SqliteDataSource = ConvertFrom-YamlScalar $Matches[1]
134+
continue
135+
}
136+
}
137+
138+
return @{
139+
HasMcp = $hasMcp
140+
Instances = $instances
141+
}
142+
}
143+
144+
function ConvertTo-McpInstanceMap {
145+
<#
146+
.SYNOPSIS
147+
Normalizes parsed instance settings into a consistent ordered dictionary.
148+
149+
.DESCRIPTION
150+
Converts either JSON-derived PSCustomObject instances or the YAML parser output into
151+
the same RepoRoot/Port/TodoStorage shape so the validation logic can iterate a single
152+
data structure regardless of the source file format.
153+
#>
154+
param(
155+
[object]$Instances
156+
)
157+
158+
$instanceMap = [ordered]@{}
159+
if ($null -eq $Instances) {
160+
return $instanceMap
161+
}
162+
163+
$entries = if ($Instances -is [System.Collections.IDictionary]) {
164+
$Instances.GetEnumerator() | Sort-Object Name
165+
}
166+
else {
167+
$Instances.PSObject.Properties | ForEach-Object {
168+
[pscustomobject]@{
169+
Name = $_.Name
170+
Value = $_.Value
171+
}
172+
} | Sort-Object Name
173+
}
174+
175+
foreach ($entry in $entries) {
176+
$instanceMap[$entry.Name] = [ordered]@{
177+
RepoRoot = $entry.Value.RepoRoot
178+
Port = $entry.Value.Port
179+
TodoStorage = [ordered]@{
180+
Provider = $entry.Value.TodoStorage.Provider
181+
SqliteDataSource = $entry.Value.TodoStorage.SqliteDataSource
182+
}
183+
}
184+
}
185+
186+
return $instanceMap
187+
}
188+
189+
if ([string]::IsNullOrWhiteSpace($ConfigPath)) {
190+
$candidatePaths = @(
191+
"src/McpServer.Support.Mcp/appsettings.yaml",
192+
"src/McpServer.Support.Mcp/appsettings.yml",
193+
"src/McpServer.Support.Mcp/appsettings.json"
194+
)
195+
$ConfigPath = $candidatePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
196+
}
197+
198+
if (-not (Test-Path $ConfigPath)) {
199+
throw "Config file not found: $ConfigPath"
200+
}
201+
202+
$extension = [System.IO.Path]::GetExtension($ConfigPath)
203+
$config = switch ($extension.ToLowerInvariant()) {
204+
".yaml" { Get-McpInstancesFromYaml -Path $ConfigPath }
205+
".yml" { Get-McpInstancesFromYaml -Path $ConfigPath }
206+
".json" {
207+
$json = Get-Content -Raw -Path $ConfigPath | ConvertFrom-Json
208+
@{
209+
HasMcp = $null -ne $json.Mcp
210+
Instances = ConvertTo-McpInstanceMap -Instances $json.Mcp.Instances
211+
}
212+
}
213+
default { throw "Unsupported config format '$extension' for '$ConfigPath'." }
214+
}
215+
216+
if (-not $config.HasMcp) {
217+
throw "Missing 'Mcp' section."
218+
}
219+
220+
$instances = $config.Instances
221+
if (-not $instances) {
222+
Write-Host "No Mcp:Instances configured. Validation passed."
223+
exit 0
224+
}
225+
226+
$instanceEntries = $instances.GetEnumerator() | Sort-Object Name
227+
228+
$ports = @{}
229+
$instanceEntries | ForEach-Object {
230+
$name = $_.Name
231+
$instance = $_.Value
232+
233+
if (-not $instance.RepoRoot) {
234+
throw "Instance '$name' missing RepoRoot."
34235
}
35236
$resolvedRoot = [System.IO.Path]::GetFullPath([string]$instance.RepoRoot)
36237
if (-not (Test-Path -Path $resolvedRoot -PathType Container)) {
@@ -58,14 +259,14 @@ $instances.PSObject.Properties | ForEach-Object {
58259

59260
if ($provider -eq "sqlite") {
60261
$sqliteDataSource = ""
61-
if ($instance.TodoStorage -and $instance.TodoStorage.SqliteDataSource) {
62-
$sqliteDataSource = [string]$instance.TodoStorage.SqliteDataSource
63-
}
64-
if ([string]::IsNullOrWhiteSpace($sqliteDataSource)) {
65-
throw "Instance '$name' provider sqlite requires TodoStorage.SqliteDataSource."
66-
}
67-
}
68-
}
69-
70-
$instanceCount = @($instances.PSObject.Properties).Count
71-
Write-Host "MCP config validation passed for $instanceCount instances."
262+
if ($instance.TodoStorage -and $instance.TodoStorage.SqliteDataSource) {
263+
$sqliteDataSource = [string]$instance.TodoStorage.SqliteDataSource
264+
}
265+
if ([string]::IsNullOrWhiteSpace($sqliteDataSource)) {
266+
throw "Instance '$name' provider sqlite requires TodoStorage.SqliteDataSource."
267+
}
268+
}
269+
}
270+
271+
$instanceCount = @($instanceEntries).Count
272+
Write-Host "MCP config validation passed for $instanceCount instances."

src/McpServer.Client/Models/ContextModels.cs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -271,9 +271,24 @@ public sealed class GraphRagStatusResult
271271
[JsonPropertyName("lastIndexedDocumentCount")]
272272
public int? LastIndexedDocumentCount { get; set; }
273273

274-
[JsonPropertyName("backend")]
275-
public string Backend { get; set; } = string.Empty;
276-
}
274+
[JsonPropertyName("backend")]
275+
public string Backend { get; set; } = string.Empty;
276+
277+
[JsonPropertyName("indexCorpus")]
278+
public string IndexCorpus { get; set; } = string.Empty;
279+
280+
[JsonPropertyName("queryCorpus")]
281+
public string QueryCorpus { get; set; } = string.Empty;
282+
283+
[JsonPropertyName("inputPath")]
284+
public string InputPath { get; set; } = string.Empty;
285+
286+
[JsonPropertyName("inputDocumentCount")]
287+
public int InputDocumentCount { get; set; }
288+
289+
[JsonPropertyName("visibilityNote")]
290+
public string? VisibilityNote { get; set; }
291+
}
277292

278293
/// <summary>GraphRAG citation entry.</summary>
279294
public sealed class GraphRagCitation
@@ -327,8 +342,14 @@ public sealed class GraphRagQueryResult
327342
[JsonPropertyName("failureCode")]
328343
public string? FailureCode { get; set; }
329344

330-
[JsonPropertyName("backend")]
331-
public string Backend { get; set; } = string.Empty;
332-
}
345+
[JsonPropertyName("backend")]
346+
public string Backend { get; set; } = string.Empty;
347+
348+
[JsonPropertyName("queryCorpus")]
349+
public string QueryCorpus { get; set; } = string.Empty;
350+
351+
[JsonPropertyName("visibilityNote")]
352+
public string? VisibilityNote { get; set; }
353+
}
333354

334355
#pragma warning restore CS1591

src/McpServer.GraphRag/Models/GraphRagModels.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public sealed class GraphRagStatusResponse
4040
public long? LastIndexDurationMs { get; set; }
4141
public int? LastIndexedDocumentCount { get; set; }
4242
public string Backend { get; set; } = "internal-fallback";
43+
public string IndexCorpus { get; set; } = "graphrag-input";
44+
public string QueryCorpus { get; set; } = "context-search";
45+
public string InputPath { get; set; } = string.Empty;
46+
public int InputDocumentCount { get; set; }
47+
public string? VisibilityNote { get; set; }
4348
}
4449

4550
/// <summary>Citation payload from GraphRAG query responses.</summary>
@@ -66,6 +71,8 @@ public sealed class GraphRagQueryResponse
6671
public string? FallbackReason { get; set; }
6772
public string? FailureCode { get; set; }
6873
public string Backend { get; set; } = "internal-fallback";
74+
public string QueryCorpus { get; set; } = "context-search";
75+
public string? VisibilityNote { get; set; }
6976
}
7077

7178
#pragma warning restore CS1591

src/McpServer.GraphRag/Services/ExternalCommandGraphRagBackendAdapter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ public async Task<GraphRagBackendIndexResult> IndexAsync(GraphRagBackendExecutio
8585
FallbackUsed = false,
8686
FallbackReason = null,
8787
FailureCode = null,
88-
Backend = AdapterName
88+
Backend = AdapterName,
89+
QueryCorpus = "graphrag-backend",
90+
VisibilityNote = null
8991
};
9092
}
9193
catch (Exception ex) when (ex is not OperationCanceledException)

0 commit comments

Comments
 (0)