@@ -710,6 +710,181 @@ function Test-McpModeWithoutSolutionDir {
710710 }
711711}
712712
713+ function Test-McpModeWithRootsFallback {
714+ param (
715+ [string ]$SlnDir ,
716+ [int ]$DefaultPort ,
717+ [int ]$MaxAllocatedPort ,
718+ [int ]$CheckAttempts = 5 ,
719+ [int ]$MaxAttempts = 30
720+ )
721+
722+ Write-Log " Validating MCP mode with --force-roots-fallback: devserver should start only after SetRoots is called"
723+
724+ $mcpTestPort = if ($MaxAllocatedPort -ge 65533 ) { $DefaultPort + 3 } else { $MaxAllocatedPort + 1 }
725+
726+ $process = $null
727+ $mcpProcessStarted = $false
728+ $stdoutLogPath = [System.IO.Path ]::GetTempFileName()
729+ $stderrLogPath = [System.IO.Path ]::GetTempFileName()
730+
731+ try {
732+ # Start MCP with --force-roots-fallback flag - devserver should NOT start until roots are provided
733+ $mcpArguments = Get-DevserverCliArguments - Arguments @ (" --mcp-app" , " --force-roots-fallback" , " --port" , $mcpTestPort.ToString ([System.Globalization.CultureInfo ]::InvariantCulture), " -l" , " trace" )
734+
735+ $startInfo = New-Object System.Diagnostics.ProcessStartInfo
736+ $startInfo.FileName = $script :DevServerHostExecutable
737+ foreach ($arg in $mcpArguments ) {
738+ [void ]$startInfo.ArgumentList.Add ($arg )
739+ }
740+ $startInfo.WorkingDirectory = $SlnDir
741+ $startInfo.UseShellExecute = $false
742+ $startInfo.CreateNoWindow = $true
743+ $startInfo.RedirectStandardOutput = $true
744+ $startInfo.RedirectStandardError = $true
745+ $startInfo.RedirectStandardInput = $true
746+ $startInfo.StandardOutputEncoding = [System.Text.Encoding ]::UTF8
747+ $startInfo.StandardErrorEncoding = [System.Text.Encoding ]::UTF8
748+
749+ $process = New-Object System.Diagnostics.Process
750+ $process.StartInfo = $startInfo
751+
752+ if (-not $process.Start ()) {
753+ throw " Failed to start MCP proxy process for roots-fallback test."
754+ }
755+ $mcpProcessStarted = $true
756+
757+ # Start background tasks to capture stdout/stderr to files
758+ $stdoutStream = New-Object System.IO.FileStream($stdoutLogPath , [System.IO.FileMode ]::Create, [System.IO.FileAccess ]::Write, [System.IO.FileShare ]::ReadWrite)
759+ $stderrStream = New-Object System.IO.FileStream($stderrLogPath , [System.IO.FileMode ]::Create, [System.IO.FileAccess ]::Write, [System.IO.FileShare ]::ReadWrite)
760+ $cts = New-Object System.Threading.CancellationTokenSource
761+ $stdoutTask = $process.StandardOutput.BaseStream.CopyToAsync ($stdoutStream , 81920 , $cts.Token )
762+ $stderrTask = $process.StandardError.BaseStream.CopyToAsync ($stderrStream , 81920 , $cts.Token )
763+
764+ # Give the MCP proxy a moment to initialize
765+ Start-Sleep - Seconds 5
766+
767+ # PHASE 1: Verify the devserver does NOT appear in the list (because roots-fallback is enabled and no roots were set)
768+ Write-Log " Phase 1: Verifying devserver did NOT auto-start..."
769+ $foundInList = $false
770+ for ($attempt = 1 ; $attempt -le $CheckAttempts ; $attempt ++ ) {
771+ Write-Log " Checking devserver list for absence of auto-started instance (attempt $attempt /$CheckAttempts )..."
772+
773+ $listOutput = " "
774+ try {
775+ $listOutput = Invoke-DevserverCli - Arguments @ (' list' ) 2>&1
776+ }
777+ catch {
778+ $listOutput = $_.Exception.Message
779+ }
780+
781+ $listText = ($listOutput | Out-String )
782+ $portPattern = " Port\s*:\s*$mcpTestPort "
783+ $endpointPattern = " :$mcpTestPort (\b|/)"
784+
785+ if ($LASTEXITCODE -eq 0 -and ($listText -match $portPattern -or $listText -match $endpointPattern )) {
786+ $foundInList = $true
787+ break
788+ }
789+
790+ Start-Sleep - Seconds 2
791+ }
792+
793+ if ($foundInList ) {
794+ $stdoutLog = if (Test-Path $stdoutLogPath ) { Get-Content $stdoutLogPath - Raw - ErrorAction SilentlyContinue } else { " " }
795+ $stderrLog = if (Test-Path $stderrLogPath ) { Get-Content $stderrLogPath - Raw - ErrorAction SilentlyContinue } else { " " }
796+ throw " Devserver with --force-roots-fallback should NOT have started automatically, but it appeared in 'uno-devserver list'.`n STDOUT:`n $stdoutLog `n STDERR:`n $stderrLog "
797+ }
798+
799+ Write-Log " Phase 1 passed: Devserver did NOT auto-start (as expected)"
800+
801+ # PHASE 2: Send MCP initialize and SetRoots call via STDIO, then verify devserver starts
802+ Write-Log " Phase 2: Sending MCP initialize and SetRoots via STDIO..."
803+
804+ # MCP JSON-RPC initialize request
805+ $initializeRequest = @ {
806+ jsonrpc = " 2.0"
807+ id = 1
808+ method = " initialize"
809+ params = @ {
810+ protocolVersion = " 2024-11-05"
811+ capabilities = @ {}
812+ clientInfo = @ {
813+ name = " test-client"
814+ version = " 1.0.0"
815+ }
816+ }
817+ } | ConvertTo-Json - Depth 10 - Compress
818+
819+ # MCP JSON-RPC tools/call request for uno_app_set_roots
820+ $normalizedSlnDir = $SlnDir -replace ' \\' , ' /'
821+ $setRootsRequest = @ {
822+ jsonrpc = " 2.0"
823+ id = 2
824+ method = " tools/call"
825+ params = @ {
826+ name = " uno_app_set_roots"
827+ arguments = @ {
828+ roots = @ ($normalizedSlnDir )
829+ }
830+ }
831+ } | ConvertTo-Json - Depth 10 - Compress
832+
833+ # Write requests to stdin (each message is a single line in MCP STDIO transport)
834+ $stdinWriter = $process.StandardInput
835+ $stdinWriter.WriteLine ($initializeRequest )
836+ $stdinWriter.Flush ()
837+
838+ Start-Sleep - Seconds 2
839+
840+ $stdinWriter.WriteLine ($setRootsRequest )
841+ $stdinWriter.Flush ()
842+
843+ Write-Log " Sent SetRoots request with root: $normalizedSlnDir "
844+
845+ # PHASE 3: Verify the devserver NOW appears in the list
846+ Write-Log " Phase 3: Verifying devserver started after SetRoots..."
847+ $mcpStarted = Wait-ForDevserverListEntry - Port $mcpTestPort - SolutionDirectory $SlnDir - MaxAttempts $MaxAttempts - DelaySeconds 2
848+
849+ if (-not $mcpStarted ) {
850+ $stdoutLog = if (Test-Path $stdoutLogPath ) { Get-Content $stdoutLogPath - Raw - ErrorAction SilentlyContinue } else { " " }
851+ $stderrLog = if (Test-Path $stderrLogPath ) { Get-Content $stderrLogPath - Raw - ErrorAction SilentlyContinue } else { " " }
852+ throw " Devserver with --force-roots-fallback did NOT start after SetRoots was called via MCP STDIO.`n STDOUT:`n $stdoutLog `n STDERR:`n $stderrLog "
853+ }
854+
855+ Write-Log " Phase 3 passed: Devserver started after SetRoots call"
856+ Write-Log " Test-McpModeWithRootsFallback completed successfully"
857+ }
858+ finally {
859+ # Clean up
860+ if ($cts ) {
861+ try { $cts.Cancel () } catch {}
862+ try { $cts.Dispose () } catch {}
863+ }
864+
865+ if ($process -and $mcpProcessStarted -and -not $process.HasExited ) {
866+ try { $process.Kill () } catch {}
867+ try { $process.WaitForExit (5000 ) } catch {}
868+ }
869+
870+ if ($stdoutStream ) {
871+ try { $stdoutStream.Flush () } catch {}
872+ try { $stdoutStream.Dispose () } catch {}
873+ }
874+ if ($stderrStream ) {
875+ try { $stderrStream.Flush () } catch {}
876+ try { $stderrStream.Dispose () } catch {}
877+ }
878+
879+ Stop-DevserverInDirectory - Directory $SlnDir
880+
881+ if (Test-Path $stdoutLogPath ) { Remove-Item $stdoutLogPath - ErrorAction SilentlyContinue }
882+ if (Test-Path $stderrLogPath ) { Remove-Item $stderrLogPath - ErrorAction SilentlyContinue }
883+ }
884+
885+ return $mcpTestPort
886+ }
887+
713888function Stop-DevserverInDirectory {
714889 param ([string ]$Directory )
715890
@@ -796,6 +971,8 @@ try {
796971 $primaryPort = Test-DevserverStartStop - SlnDir $slnDir - CsprojDir $csprojDir - CsprojPath $csprojPath - DefaultPort $defaultPort - MaxAttempts $maxAttempts
797972 $solutionDirTestPort = Test-DevserverSolutionDirSupport - SlnDir $slnDir - DefaultPort $defaultPort - BaselinePort $primaryPort - MaxAttempts $maxAttempts
798973 Test-McpModeWithoutSolutionDir - SlnDir $slnDir - DefaultPort $defaultPort - PrimaryPort $primaryPort - SolutionDirPort $solutionDirTestPort - MaxAttempts $maxAttempts
974+ $maxAllocatedPort = [Math ]::Max($primaryPort , $solutionDirTestPort )
975+ Test-McpModeWithRootsFallback - SlnDir $slnDir - DefaultPort $defaultPort - MaxAllocatedPort $maxAllocatedPort
799976 Test-CodexIntegration - SlnDir $slnDir
800977
801978 $finalExitCode = 0
0 commit comments