Skip to content

Commit 2417323

Browse files
gfraiteurclaude
andcommitted
Improve MCP server process management with PID file tracking
Use PID files to reliably track and terminate MCP server processes instead of relying on netstat port lookups. This fixes file locking issues when restarting the container and allows multiple repos to run their own MCP servers concurrently. Key changes: - Store MCP server PID in deterministic file based on repo hash - Kill existing MCP server before build to prevent file locks - Use -EncodedCommand for PowerShell to avoid quoting issues with wt.exe - Verify executable exists before starting server - Improved cleanup with proper error handling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8ac57e0 commit 2417323

2 files changed

Lines changed: 260 additions & 84 deletions

File tree

DockerBuild.ps1

Lines changed: 131 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ param(
2828

2929
####
3030
# These settings are replaced by the generate-scripts command.
31-
$EngPath = 'eng'
32-
$EnvironmentVariables = 'AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AZ_IDENTITY_USERNAME,AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_DEVOPS_TOKEN,AZURE_DEVOPS_USER,AZURE_TENANT_ID,DOC_API_KEY,DOWNLOADS_API_KEY,ENG_USERNAME,GIT_USER_EMAIL,GIT_USER_NAME,GITHUB_AUTHOR_EMAIL,GITHUB_REVIEWER_TOKEN,GITHUB_TOKEN,IS_POSTSHARP_OWNED,IS_TEAMCITY_AGENT,MetalamaLicense,NUGET_ORG_API_KEY,PostSharpLicense,SIGNSERVER_SECRET,TEAMCITY_CLOUD_TOKEN,TYPESENSE_API_KEY,VS_MARKETPLACE_ACCESS_TOKEN,VSS_NUGET_EXTERNAL_FEED_ENDPOINTS'
31+
$EngPath = '<ENG_PATH>'
32+
$EnvironmentVariables = '<ENVIRONMENT_VARIABLES>'
3333
####
3434

3535
$ErrorActionPreference = "Stop"
@@ -335,9 +335,32 @@ function Copy-McpServerToTemp
335335
$hashBytes = (New-Object -TypeName System.Security.Cryptography.SHA256Managed).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($SourceRootDir))
336336
$directoryHash = [System.BitConverter]::ToString($hashBytes, 0, 4).Replace("-", "").ToLower()
337337
$tempDir = Join-Path $env:TEMP "mcp-server-$directoryHash"
338+
$mcpPidFile = Join-Path $env:TEMP "mcp-pid-$directoryHash.txt"
339+
340+
# Kill existing MCP server for this repo if running
341+
if (Test-Path $mcpPidFile)
342+
{
343+
try
344+
{
345+
$existingPid = (Get-Content $mcpPidFile -Raw).Trim()
346+
if ($existingPid -match '^\d+$')
347+
{
348+
$process = Get-Process -Id $existingPid -ErrorAction SilentlyContinue
349+
if ($process)
350+
{
351+
Write-Host "Killing existing MCP server (PID: $existingPid)..." -ForegroundColor Yellow
352+
Stop-Process -Id $existingPid -Force -ErrorAction SilentlyContinue
353+
Start-Sleep -Milliseconds 1000
354+
}
355+
}
356+
}
357+
catch
358+
{
359+
Write-Host "Could not kill existing MCP server: $_" -ForegroundColor Yellow
360+
}
361+
}
338362

339363
# Clean up old temp directory if it exists
340-
# Use retry logic because files may be locked by a previous MCP server process
341364
if (Test-Path $tempDir)
342365
{
343366
$maxRetries = 3
@@ -369,6 +392,12 @@ function Copy-McpServerToTemp
369392
}
370393
}
371394

395+
# Clean up stale PID file
396+
if (Test-Path $mcpPidFile)
397+
{
398+
Remove-Item $mcpPidFile -Force -ErrorAction SilentlyContinue
399+
}
400+
372401
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
373402
Write-Host "Created temporary directory: $tempDir" -ForegroundColor Cyan
374403

@@ -379,9 +408,18 @@ function Copy-McpServerToTemp
379408

380409
# Return the path to the executable and the temp directory for cleanup
381410
$tempExecutable = Join-Path $tempTargetDir $executableFile.Name
411+
412+
# Verify the executable was copied successfully
413+
if (-not (Test-Path $tempExecutable))
414+
{
415+
throw "Failed to copy MCP server executable to temporary directory. Expected at: $tempExecutable"
416+
}
417+
Write-Host "Verified MCP server executable at: $tempExecutable" -ForegroundColor Cyan
418+
382419
return @{
383420
ExecutablePath = $tempExecutable
384421
TempDirectory = $tempDir
422+
DirectoryHash = $directoryHash
385423
IsExe = $executableFile.Extension -eq ".exe"
386424
}
387425
}
@@ -471,6 +509,34 @@ if ($Claude -and -not $NoMcp)
471509
{
472510
try
473511
{
512+
# Kill existing MCP server BEFORE building (it may have files locked)
513+
# Compute the hash to find the PID file
514+
$hashBytes = (New-Object -TypeName System.Security.Cryptography.SHA256Managed).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($PSScriptRoot))
515+
$mcpDirHash = [System.BitConverter]::ToString($hashBytes, 0, 4).Replace("-", "").ToLower()
516+
$mcpPidFilePath = Join-Path $env:TEMP "mcp-pid-$mcpDirHash.txt"
517+
518+
if (Test-Path $mcpPidFilePath)
519+
{
520+
try
521+
{
522+
$existingPid = (Get-Content $mcpPidFilePath -Raw).Trim()
523+
if ($existingPid -match '^\d+$')
524+
{
525+
$process = Get-Process -Id $existingPid -ErrorAction SilentlyContinue
526+
if ($process)
527+
{
528+
Write-Host "Killing existing MCP server (PID: $existingPid) before build..." -ForegroundColor Yellow
529+
Stop-Process -Id $existingPid -Force -ErrorAction SilentlyContinue
530+
Start-Sleep -Milliseconds 1000
531+
}
532+
}
533+
}
534+
catch
535+
{
536+
Write-Host "Could not kill existing MCP server: $_" -ForegroundColor Yellow
537+
}
538+
}
539+
474540
Write-Host "Building MCP server before cleanup..." -ForegroundColor Cyan
475541
$mcpProjectPath = Join-Path $PSScriptRoot "$EngPath\src"
476542

@@ -1117,6 +1183,7 @@ if (-not $BuildImage)
11171183
# Start MCP approval server on host with dynamic port in new terminal tab
11181184
$mcpPort = $null
11191185
$mcpPortFile = $null
1186+
$mcpPidFile = $null
11201187
$mcpSecret = $null
11211188
$mcpTempDir = $null
11221189
if (-not $NoMcp)
@@ -1130,7 +1197,21 @@ if (-not $BuildImage)
11301197
}
11311198

11321199
Write-Host "Starting MCP approval server..." -ForegroundColor Green
1133-
$mcpPortFile = Join-Path $env:TEMP "mcp-port-$([System.Guid]::NewGuid().ToString('N').Substring(0, 8) ).txt"
1200+
1201+
# Use the MCP server snapshot saved before cleanup
1202+
$mcpServerInfo = $mcpServerSnapshot
1203+
$mcpTempDir = $mcpServerInfo.TempDirectory
1204+
$directoryHash = $mcpServerInfo.DirectoryHash
1205+
1206+
# Verify the executable still exists
1207+
if (-not (Test-Path $mcpServerInfo.ExecutablePath))
1208+
{
1209+
throw "MCP server executable not found at: $($mcpServerInfo.ExecutablePath). The temporary directory may have been cleaned up."
1210+
}
1211+
1212+
# Use deterministic file paths based on repo hash (allows multiple repos to have their own MCP servers)
1213+
$mcpPortFile = Join-Path $env:TEMP "mcp-port-$directoryHash.txt"
1214+
$mcpPidFile = Join-Path $env:TEMP "mcp-pid-$directoryHash.txt"
11341215

11351216
# Generate 128-bit (16 byte) random secret for authentication
11361217
$randomBytes = New-Object byte[] 16
@@ -1139,38 +1220,39 @@ if (-not $BuildImage)
11391220
$mcpSecret = [BitConverter]::ToString($randomBytes).Replace('-', '').ToLower()
11401221
Write-Host "Generated MCP authentication secret" -ForegroundColor Cyan
11411222

1142-
# Use the MCP server snapshot saved before cleanup
1143-
$mcpServerInfo = $mcpServerSnapshot
1144-
$mcpTempDir = $mcpServerInfo.TempDirectory
1145-
11461223
# Build the command to run in the new tab
1224+
# Start the MCP server and capture its actual PID (not the PowerShell host PID)
11471225
if ($mcpServerInfo.IsExe)
11481226
{
1149-
# Run executable directly
1150-
$mcpCommand = "& '$( $mcpServerInfo.ExecutablePath )' tools mcp-server --port-file '$mcpPortFile' --secret '$mcpSecret'"
1227+
# Run executable directly - use Start-Process to capture the actual process PID
1228+
$mcpCommand = "`$proc = Start-Process -FilePath '$( $mcpServerInfo.ExecutablePath )' -ArgumentList 'tools','mcp-server','--port-file','$mcpPortFile','--secret','$mcpSecret' -PassThru -NoNewWindow; `$proc.Id | Set-Content -Path '$mcpPidFile' -NoNewline; Wait-Process -Id `$proc.Id"
11511229
}
11521230
else
11531231
{
1154-
# Run DLL with dotnet
1155-
$mcpCommand = "dotnet '$( $mcpServerInfo.ExecutablePath )' tools mcp-server --port-file '$mcpPortFile' --secret '$mcpSecret'"
1232+
# Run DLL with dotnet - use Start-Process to capture the actual process PID
1233+
$mcpCommand = "`$proc = Start-Process -FilePath 'dotnet' -ArgumentList '$( $mcpServerInfo.ExecutablePath )','tools','mcp-server','--port-file','$mcpPortFile','--secret','$mcpSecret' -PassThru -NoNewWindow; `$proc.Id | Set-Content -Path '$mcpPidFile' -NoNewline; Wait-Process -Id `$proc.Id"
11561234
}
11571235

1236+
# Encode command as base64 to avoid quoting issues when passing through wt.exe
1237+
$mcpCommandBytes = [System.Text.Encoding]::Unicode.GetBytes($mcpCommand)
1238+
$mcpCommandBase64 = [Convert]::ToBase64String($mcpCommandBytes)
1239+
11581240
# Try Windows Terminal first (wt.exe), fall back to conhost
11591241
$wtPath = Get-Command wt.exe -ErrorAction SilentlyContinue
11601242
if ($wtPath)
11611243
{
11621244
# Open new tab in current Windows Terminal window
11631245
# The -w 0 option targets the current window
1164-
# Use single argument string for proper escaping
1246+
# Use -EncodedCommand to avoid quoting issues with complex commands
11651247
# NOTE: --startingDirectory must be specified because Start-Process's -WorkingDirectory doesn't pass through to wt.exe
1166-
$wtArgString = "-w 0 new-tab --title `"MCP Approval Server - $PSScriptRoot`" --startingDirectory `"$PSScriptRoot`" -- pwsh -NoExit -Command `"$mcpCommand`""
1248+
$wtArgString = "-w 0 new-tab --title `"MCP Approval Server - $PSScriptRoot`" --startingDirectory `"$PSScriptRoot`" -- pwsh -NoExit -EncodedCommand $mcpCommandBase64"
11671249
$mcpServerProcess = Start-Process -FilePath "wt.exe" -ArgumentList $wtArgString -PassThru
11681250
}
11691251
else
11701252
{
1171-
# Fallback: start in new console window
1253+
# Fallback: start in new console window (use EncodedCommand here too for consistency)
11721254
$mcpServerProcess = Start-Process -FilePath "pwsh" `
1173-
-ArgumentList "-NoExit", "-Command", $mcpCommand `
1255+
-ArgumentList "-NoExit", "-EncodedCommand", $mcpCommandBase64 `
11741256
-WorkingDirectory $PSScriptRoot `
11751257
-PassThru
11761258
}
@@ -1202,6 +1284,10 @@ if (-not $BuildImage)
12021284
{
12031285
Stop-Process -Id $mcpServerProcess.Id -Force -ErrorAction SilentlyContinue
12041286
}
1287+
if ($mcpPidFile -and (Test-Path $mcpPidFile))
1288+
{
1289+
Remove-Item $mcpPidFile -Force -ErrorAction SilentlyContinue
1290+
}
12051291
if ($mcpTempDir -and (Test-Path $mcpTempDir))
12061292
{
12071293
Remove-Item $mcpTempDir -Recurse -Force -ErrorAction SilentlyContinue
@@ -1385,41 +1471,23 @@ if (-not $BuildImage)
13851471
{
13861472
Write-Host "Stopping MCP approval server..." -ForegroundColor Cyan
13871473

1388-
# Find the process listening on the MCP port and kill it
1389-
try
1474+
# Kill MCP server using PID file
1475+
if ($mcpPidFile -and (Test-Path $mcpPidFile))
13901476
{
1391-
# Find PID using netstat
1392-
$netstatOutput = netstat -ano | Select-String ":$mcpPort\s" | Select-Object -First 1
1393-
if ($netstatOutput)
1477+
try
13941478
{
1395-
$parts = $netstatOutput.Line.Trim() -split '\s+'
1396-
$mcpPid = $parts[-1]
1397-
if ($mcpPid -and $mcpPid -match '^\d+$')
1479+
$mcpPid = (Get-Content $mcpPidFile -Raw).Trim()
1480+
if ($mcpPid -match '^\d+$')
13981481
{
13991482
Stop-Process -Id $mcpPid -Force -ErrorAction SilentlyContinue
14001483
Write-Host "Stopped MCP server process (PID: $mcpPid)" -ForegroundColor Cyan
1484+
# Wait for process to fully release file locks
1485+
Start-Sleep -Milliseconds 1000
14011486
}
14021487
}
1403-
}
1404-
catch
1405-
{
1406-
Write-Host "Could not stop MCP server via port lookup: $_" -ForegroundColor Yellow
1407-
}
1408-
1409-
# Fallback: try to find by command line
1410-
$mcpProcesses = Get-Process -Name pwsh, dotnet -ErrorAction SilentlyContinue |
1411-
Where-Object { $_.CommandLine -like "*mcp-server*" }
1412-
1413-
foreach ($proc in $mcpProcesses)
1414-
{
1415-
try
1416-
{
1417-
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
1418-
Write-Host "Stopped MCP server process $( $proc.Id )" -ForegroundColor Cyan
1419-
}
14201488
catch
14211489
{
1422-
# Process may have already exited
1490+
Write-Host "Could not stop MCP server: $_" -ForegroundColor Yellow
14231491
}
14241492
}
14251493
}
@@ -1431,10 +1499,30 @@ if (-not $BuildImage)
14311499
}
14321500

14331501
# Clean up temporary MCP server directory
1502+
# Only delete PID file if temp directory cleanup succeeds (so next run can still kill the process if needed)
1503+
$tempDirCleaned = $false
14341504
if ($mcpTempDir -and (Test-Path $mcpTempDir))
14351505
{
14361506
Write-Host "Cleaning up temporary MCP server directory: $mcpTempDir" -ForegroundColor Cyan
1437-
Remove-Item $mcpTempDir -Recurse -Force -ErrorAction SilentlyContinue
1507+
try
1508+
{
1509+
Remove-Item $mcpTempDir -Recurse -Force -ErrorAction Stop
1510+
$tempDirCleaned = $true
1511+
}
1512+
catch
1513+
{
1514+
Write-Host "Could not clean up temp directory (will retry on next run): $_" -ForegroundColor Yellow
1515+
}
1516+
}
1517+
else
1518+
{
1519+
$tempDirCleaned = $true
1520+
}
1521+
1522+
# Only clean up PID file if temp directory was successfully cleaned
1523+
if ($tempDirCleaned -and $mcpPidFile -and (Test-Path $mcpPidFile))
1524+
{
1525+
Remove-Item $mcpPidFile -ErrorAction SilentlyContinue
14381526
}
14391527
}
14401528
}

0 commit comments

Comments
 (0)