|
10 | 10 | [switch]$NoNuGetCache, # Does not mount the host nuget cache in the container. |
11 | 11 | [switch]$KeepEnv, # Does not override the env.g.json file. |
12 | 12 | [switch]$Claude, # Run Claude CLI instead of Build.ps1. Use -Claude for interactive, -Claude "prompt" for non-interactive. |
| 13 | + [switch]$NoMcp, # Do not start the MCP approval server (for -Claude mode). |
13 | 14 | [string]$ImageName, # Image name (defaults to a name based on the directory). |
14 | 15 | [string]$BuildAgentPath = 'C:\BuildAgent', |
15 | 16 | [switch]$LoadEnvFromKeyVault, # Forces loading environment variables form the key vault. |
@@ -332,6 +333,54 @@ if (Test-Path $sourceDependenciesDir) |
332 | 333 | } |
333 | 334 | } |
334 | 335 |
|
| 336 | +# Mount sibling directories from the product family (parent directory) |
| 337 | +# Only if parent is a recognized product family (PostSharp* or Metalama*) |
| 338 | +$parentDir = Split-Path $SourceDirName -Parent |
| 339 | +$parentDirName = Split-Path $parentDir -Leaf |
| 340 | +if ($parentDir -and (Test-Path $parentDir) -and ($parentDirName -like "PostSharp*" -or $parentDirName -like "Metalama*")) |
| 341 | +{ |
| 342 | + Write-Host "Detected product family directory: $parentDirName" -ForegroundColor Cyan |
| 343 | + $siblingDirs = Get-ChildItem -Path $parentDir -Directory -ErrorAction SilentlyContinue | |
| 344 | + Where-Object { $_.FullName -ne $SourceDirName } |
| 345 | + |
| 346 | + foreach ($sibling in $siblingDirs) |
| 347 | + { |
| 348 | + $siblingPath = $sibling.FullName |
| 349 | + # Skip if already mounted |
| 350 | + $alreadyMounted = $VolumeMappings | Where-Object { $_ -like "*${siblingPath}:*" } |
| 351 | + if (-not $alreadyMounted) |
| 352 | + { |
| 353 | + Write-Host "Mounting product family sibling: $siblingPath" -ForegroundColor Cyan |
| 354 | + $VolumeMappings += @("-v", "${siblingPath}:${siblingPath}:ro") |
| 355 | + $MountPoints += $siblingPath |
| 356 | + $GitDirectories += $siblingPath |
| 357 | + } |
| 358 | + } |
| 359 | +} |
| 360 | + |
| 361 | +# Mount PostSharp.Engineering.* directories from grandparent |
| 362 | +# This provides access to engineering tools and related repos |
| 363 | +$grandparentDir = Split-Path $parentDir -Parent |
| 364 | +if ($grandparentDir -and (Test-Path $grandparentDir)) |
| 365 | +{ |
| 366 | + $engineeringDirs = Get-ChildItem -Path $grandparentDir -Directory -Filter "PostSharp.Engineering*" -ErrorAction SilentlyContinue | |
| 367 | + Where-Object { $_.FullName -ne $SourceDirName } |
| 368 | + |
| 369 | + foreach ($engDir in $engineeringDirs) |
| 370 | + { |
| 371 | + $engPath = $engDir.FullName |
| 372 | + # Skip if already mounted |
| 373 | + $alreadyMounted = $VolumeMappings | Where-Object { $_ -like "*${engPath}:*" } |
| 374 | + if (-not $alreadyMounted) |
| 375 | + { |
| 376 | + Write-Host "Mounting engineering repo: $engPath" -ForegroundColor Cyan |
| 377 | + $VolumeMappings += @("-v", "${engPath}:${engPath}:ro") |
| 378 | + $MountPoints += $engPath |
| 379 | + $GitDirectories += $engPath |
| 380 | + } |
| 381 | + } |
| 382 | +} |
| 383 | + |
335 | 384 | # Execute auto-generated DockerMounts.g.ps1 script to add more directory mounts. |
336 | 385 | $dockerMountsScript = Join-Path $EngPath 'DockerMounts.g.ps1' |
337 | 386 | if (Test-Path $dockerMountsScript) |
@@ -440,6 +489,38 @@ foreach (`$dir in `$gitDirectories) { |
440 | 489 | git config --global --add safe.directory `$normalizedDir |
441 | 490 | } |
442 | 491 | } |
| 492 | +
|
| 493 | +# Configure MCP approval server if available (for Claude mode) |
| 494 | +`$mcpServerUrl = [Environment]::GetEnvironmentVariable('MCP_APPROVAL_SERVER') |
| 495 | +if (`$mcpServerUrl) { |
| 496 | + Write-Host "Configuring MCP approval server: `$mcpServerUrl" -ForegroundColor Cyan |
| 497 | +
|
| 498 | + # Read existing settings or create new |
| 499 | + `$settingsPath = "`$env:USERPROFILE\.claude\settings.json" |
| 500 | + `$settingsDir = Split-Path `$settingsPath -Parent |
| 501 | + if (-not (Test-Path `$settingsDir)) { |
| 502 | + New-Item -ItemType Directory -Path `$settingsDir -Force | Out-Null |
| 503 | + } |
| 504 | +
|
| 505 | + if (Test-Path `$settingsPath) { |
| 506 | + `$settings = Get-Content `$settingsPath -Raw | ConvertFrom-Json -AsHashtable |
| 507 | + } else { |
| 508 | + `$settings = @{} |
| 509 | + } |
| 510 | +
|
| 511 | + # Add MCP server configuration |
| 512 | + if (-not `$settings.ContainsKey('mcpServers')) { |
| 513 | + `$settings['mcpServers'] = @{} |
| 514 | + } |
| 515 | + `$settings['mcpServers']['host-approval'] = @{ |
| 516 | + 'type' = 'http' |
| 517 | + 'url' = `$mcpServerUrl |
| 518 | + } |
| 519 | +
|
| 520 | + # Write updated settings |
| 521 | + `$settings | ConvertTo-Json -Depth 10 | Set-Content `$settingsPath -Encoding UTF8 |
| 522 | + Write-Host "MCP server configured in Claude settings" -ForegroundColor Green |
| 523 | +} |
443 | 524 | "@ |
444 | 525 | $initScriptContent | Set-Content -Path $initScript -Encoding UTF8 |
445 | 526 |
|
@@ -507,6 +588,56 @@ if (-not $BuildImage) |
507 | 588 | { |
508 | 589 | if ($Claude) |
509 | 590 | { |
| 591 | + # Start MCP approval server on host with dynamic port in new terminal tab |
| 592 | + $mcpPort = $null |
| 593 | + $mcpPortFile = $null |
| 594 | + if (-not $NoMcp) { |
| 595 | + Write-Host "Starting MCP approval server..." -ForegroundColor Green |
| 596 | + $mcpPortFile = Join-Path $PSScriptRoot $dockerContextDirectory "mcp-port.txt" |
| 597 | + if (Test-Path $mcpPortFile) { |
| 598 | + Remove-Item $mcpPortFile |
| 599 | + } |
| 600 | + |
| 601 | + # Build the command to run in the new tab |
| 602 | + $mcpCommand = "& '$SourceDirName\Build.ps1' tools mcp-server --port-file '$mcpPortFile'" |
| 603 | + |
| 604 | + # Try Windows Terminal first (wt.exe), fall back to conhost |
| 605 | + $wtPath = Get-Command wt.exe -ErrorAction SilentlyContinue |
| 606 | + if ($wtPath) { |
| 607 | + # Open new tab in current Windows Terminal window |
| 608 | + # The -w 0 option targets the current window |
| 609 | + # Use single argument string for proper escaping |
| 610 | + $wtArgString = "-w 0 new-tab --title `"MCP Approval Server`" -- pwsh -NoExit -Command `"$mcpCommand`"" |
| 611 | + $mcpServerProcess = Start-Process -FilePath "wt.exe" -ArgumentList $wtArgString -PassThru |
| 612 | + } else { |
| 613 | + # Fallback: start in new console window |
| 614 | + $mcpServerProcess = Start-Process -FilePath "pwsh" ` |
| 615 | + -ArgumentList "-NoExit", "-Command", $mcpCommand ` |
| 616 | + -PassThru |
| 617 | + } |
| 618 | + |
| 619 | + # Wait for port file to be written (with timeout) |
| 620 | + $timeout = 30 |
| 621 | + $elapsed = 0 |
| 622 | + while (-not (Test-Path $mcpPortFile) -and $elapsed -lt $timeout) { |
| 623 | + Start-Sleep -Milliseconds 500 |
| 624 | + $elapsed += 0.5 |
| 625 | + } |
| 626 | + |
| 627 | + if (-not (Test-Path $mcpPortFile)) { |
| 628 | + Write-Error "MCP server failed to start within $timeout seconds" |
| 629 | + if ($mcpServerProcess -and !$mcpServerProcess.HasExited) { |
| 630 | + Stop-Process -Id $mcpServerProcess.Id -Force -ErrorAction SilentlyContinue |
| 631 | + } |
| 632 | + exit 1 |
| 633 | + } |
| 634 | + |
| 635 | + $mcpPort = (Get-Content $mcpPortFile -Raw).Trim() |
| 636 | + Write-Host "MCP approval server running on port $mcpPort" -ForegroundColor Cyan |
| 637 | + } else { |
| 638 | + Write-Host "Skipping MCP approval server (-NoMcp specified)." -ForegroundColor Yellow |
| 639 | + } |
| 640 | + |
510 | 641 | # Run Claude mode |
511 | 642 | Write-Host "Running Claude in the container." -ForegroundColor Green |
512 | 643 |
|
@@ -572,15 +703,67 @@ if (-not $BuildImage) |
572 | 703 | $pwshPath = 'C:\Program Files\PowerShell\7\pwsh.exe' |
573 | 704 |
|
574 | 705 | # Set HOME/USERPROFILE so Claude finds its config in the mounted location |
575 | | - $envArgs = @("-e", "HOME=$containerUserProfile", "-e", "USERPROFILE=$containerUserProfile") |
| 706 | + # Also pass MCP approval server URL if available |
| 707 | + $envArgs = @( |
| 708 | + "-e", "HOME=$containerUserProfile", |
| 709 | + "-e", "USERPROFILE=$containerUserProfile" |
| 710 | + ) |
| 711 | + if ($mcpPort) { |
| 712 | + $envArgs += @("-e", "MCP_APPROVAL_SERVER=http://host.docker.internal:$mcpPort") |
| 713 | + } |
576 | 714 |
|
577 | | - Write-Host "Executing: ``docker run --rm --memory=12g $dockerArgsAsString $VolumeMappingsAsString -e HOME=$containerUserProfile -e USERPROFILE=$containerUserProfile -w $ContainerSourceDir $ImageTag `"$pwshPath`" -Command `"$inlineScript`"" -ForegroundColor Cyan |
578 | | - docker run --rm --memory=12g $dockerArgs @VolumeMappings @envArgs -w $ContainerSourceDir $ImageTag $pwshPath -Command $inlineScript |
| 715 | + $mcpEnvDisplay = if ($mcpPort) { " -e MCP_APPROVAL_SERVER=http://host.docker.internal:$mcpPort" } else { "" } |
| 716 | + Write-Host "Executing: ``docker run --rm --memory=12g $dockerArgsAsString $VolumeMappingsAsString -e HOME=$containerUserProfile -e USERPROFILE=$containerUserProfile$mcpEnvDisplay -w $ContainerSourceDir $ImageTag `"$pwshPath`" -Command `"$inlineScript`"" -ForegroundColor Cyan |
579 | 717 |
|
580 | | - if ($LASTEXITCODE -ne 0) |
| 718 | + try { |
| 719 | + docker run --rm --memory=12g $dockerArgs @VolumeMappings @envArgs -w $ContainerSourceDir $ImageTag $pwshPath -Command $inlineScript |
| 720 | + $dockerExitCode = $LASTEXITCODE |
| 721 | + } |
| 722 | + finally { |
| 723 | + # Cleanup MCP server after container exits (only if it was started) |
| 724 | + if ($mcpPort) { |
| 725 | + Write-Host "Stopping MCP approval server..." -ForegroundColor Cyan |
| 726 | + |
| 727 | + # Find the process listening on the MCP port and kill it |
| 728 | + try { |
| 729 | + # Find PID using netstat |
| 730 | + $netstatOutput = netstat -ano | Select-String ":$mcpPort\s" | Select-Object -First 1 |
| 731 | + if ($netstatOutput) { |
| 732 | + $parts = $netstatOutput.Line.Trim() -split '\s+' |
| 733 | + $mcpPid = $parts[-1] |
| 734 | + if ($mcpPid -and $mcpPid -match '^\d+$') { |
| 735 | + Stop-Process -Id $mcpPid -Force -ErrorAction SilentlyContinue |
| 736 | + Write-Host "Stopped MCP server process (PID: $mcpPid)" -ForegroundColor Cyan |
| 737 | + } |
| 738 | + } |
| 739 | + } catch { |
| 740 | + Write-Host "Could not stop MCP server via port lookup: $_" -ForegroundColor Yellow |
| 741 | + } |
| 742 | + |
| 743 | + # Fallback: try to find by command line |
| 744 | + $mcpProcesses = Get-Process -Name pwsh, dotnet -ErrorAction SilentlyContinue | |
| 745 | + Where-Object { $_.CommandLine -like "*mcp-server*" } |
| 746 | + |
| 747 | + foreach ($proc in $mcpProcesses) { |
| 748 | + try { |
| 749 | + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue |
| 750 | + Write-Host "Stopped MCP server process $($proc.Id)" -ForegroundColor Cyan |
| 751 | + } catch { |
| 752 | + # Process may have already exited |
| 753 | + } |
| 754 | + } |
| 755 | + } |
| 756 | + |
| 757 | + # Clean up port file |
| 758 | + if ($mcpPortFile -and (Test-Path $mcpPortFile)) { |
| 759 | + Remove-Item $mcpPortFile -ErrorAction SilentlyContinue |
| 760 | + } |
| 761 | + } |
| 762 | + |
| 763 | + if ($dockerExitCode -ne 0) |
581 | 764 | { |
582 | | - Write-Host "Docker run (Claude) failed with exit code $LASTEXITCODE" -ForegroundColor Red |
583 | | - exit $LASTEXITCODE |
| 765 | + Write-Host "Docker run (Claude) failed with exit code $dockerExitCode" -ForegroundColor Red |
| 766 | + exit $dockerExitCode |
584 | 767 | } |
585 | 768 | } |
586 | 769 | else |
|
0 commit comments