Better Logic For Showing Auth Windows With Your Local MCP Server
Table of Contents
Just a week ago I talked about ways to access Entra ID accounts from local MCP servers. That code worked splendidly, except for one very annoying behavior - the Web Account Manager (WAM) would pop in behind the MCP client (such as Claude Desktop or Visual Studio Code).
This behavior is best demonstrated in this clip from my previous blog post:
Quick refresher on WAM windows #
For WAM to properly render the authentication dialog, it needs a window handle. My initial logic behind getting the window handle was reliant on the sample I published a while ago on the MSAL.NET documentation pages. See, that works in the context of console applications when those applications don’t run in “headless” mode. In our scenarios, when we’re using MCP clients like Cursor, Visual Studio Code, or Claude Desktop, there is no console or terminal window to speak of, so the logic from the aforementioned sample is flawed.
So what do we need to do instead? Well, I am no Raymond Chen, but I know just enough of the Windows API to experiment with an approach that turned out to work very well.
The code #
First, I created the “carrier” function - this is what I will use with the MSAL authentication broker initialization logic.
private static IntPtr GetBindingParentWindow()
{
_logger.LogInformation("Finding parent process window for authentication binding");
var currentProcessId = Environment.ProcessId;
return FindWindowInProcessHierarchy(currentProcessId);
}
Its purpose is simple - it gets the current process ID (that of the running MCP server) and passes it to the next function I have - FindWindowInProcessHierarchy
:
private static IntPtr FindWindowInProcessHierarchy(int processId, int hierarchyLevel = 0, int maxLevels = 5)
{
if (hierarchyLevel >= maxLevels)
{
_logger.LogWarning($"Reached maximum process hierarchy level ({maxLevels}), falling back to desktop window");
IntPtr desktopWindow = NativeBridge.GetDesktopWindow();
_logger.LogInformation($"Using desktop window as fallback: {desktopWindow}");
return desktopWindow;
}
_logger.LogInformation($"Looking for window in process {processId} (hierarchy level {hierarchyLevel})");
try
{
using var process = System.Diagnostics.Process.GetProcessById(processId);
// First try: get the main window directly from this process
if (process.MainWindowHandle != IntPtr.Zero)
{
_logger.LogInformation($"Found window from process {process.ProcessName} (ID: {processId}): {process.MainWindowHandle}");
return process.MainWindowHandle;
}
// Try to find any visible window from this process
IntPtr windowHandle = FindWindowForProcess(processId);
if (windowHandle != IntPtr.Zero)
{
return windowHandle;
}
// If we can't find a window, try to get the parent process
_logger.LogInformation($"No suitable window found for process {process.ProcessName} (ID: {processId}), checking parent process");
try
{
int parentProcessId = NativeBridge.GetParentProcessId(processId);
// Skip if we hit system processes or invalid ones
if (parentProcessId <= 4 || parentProcessId == processId)
{
_logger.LogInformation($"Reached system process or invalid parent (ID: {parentProcessId}), stopping hierarchy search");
IntPtr desktopWindow = NativeBridge.GetDesktopWindow();
_logger.LogInformation($"Using desktop window as fallback: {desktopWindow}");
return desktopWindow;
}
// Recursively check the parent process
return FindWindowInProcessHierarchy(parentProcessId, hierarchyLevel + 1, maxLevels);
}
catch (Exception ex)
{
_logger.LogWarning($"Failed to get parent process for {processId}: {ex.Message}");
// Fall through to desktop window
}
}
catch (Exception ex)
{
_logger.LogWarning($"Failed to access process {processId}: {ex.Message}");
// Fall through to desktop window
}
// If we've exhausted all options, return desktop window
IntPtr desktop = NativeBridge.GetDesktopWindow();
_logger.LogInformation($"Using desktop window as final fallback: {desktop}");
return desktop;
}
Bit verbose, but stay with me (if you are reproducing this - feel free to skip all the _logger
pieces, I am using that for diagnostics). The goal of this function is to walk the process tree and find relevant windows that we want to parent to. That is - we can’t just grab the first one that comes through the Windows API and call it a day, as many processes may have many different windows, including invisible ones.
The function is tasked with getting the process from the process ID that’s passed into it (also known as the PID
on Windows). It then attempts to get the main window handle. According to the documentation:
The main window is the window opened by the process that currently has the focus (the TopLevel form).
Seems relevant, so let’s keep this in mind. Next, if there is no main window handle, it will use another function I have, called FindWindowForProcess
, that will attempt to “scan” all process windows for the relevant ones:
private static IntPtr FindWindowForProcess(int processId)
{
var windowCandidates = new List<WindowCandidate>();
NativeBridge.EnumWindowsProc enumProc = (hWnd, lParam) =>
{
// We're really not interested in invisible windows.
if (!NativeBridge.IsWindowVisible(hWnd))
return true;
// No tiny windows need to be considered (bar is 50x50).
NativeBridge.RECT rect;
if (!NativeBridge.GetWindowRect(hWnd, out rect) || !rect.IsValidSize)
return true;
NativeBridge.GetWindowThreadProcessId(hWnd, out uint windowProcessId);
if (windowProcessId != processId)
return true;
var titleBuilder = new System.Text.StringBuilder(256);
NativeBridge.GetWindowText(hWnd, titleBuilder, titleBuilder.Capacity);
var title = titleBuilder.ToString();
// Add window as a candidate for potential parent
// windows that we will use to parent WAM to.
// Windows with a title are more relevant, so
// we prioritize them in our selection.
windowCandidates.Add(new WindowCandidate
{
Handle = hWnd,
ProcessId = (int)windowProcessId,
Title = title,
Size = rect.Width * rect.Height
});
return true;
};
NativeBridge.EnumWindows(enumProc, IntPtr.Zero);
var bestWindow = windowCandidates
.OrderByDescending(w => !string.IsNullOrEmpty(w.Title))
.ThenByDescending(w => w.Size)
.FirstOrDefault();
if (bestWindow != null && bestWindow.Handle != IntPtr.Zero)
{
_logger.LogInformation($"Found window for process {processId}: '{bestWindow.Title}' with handle {bestWindow.Handle}");
return bestWindow.Handle;
}
_logger.LogInformation($"No suitable windows found for process {processId}");
return IntPtr.Zero;
}
It essentially looks through the visible windows that a process has available, filtering for those larger than a minimum size (50px x 50px in this case, but this threshold can vary based on your needs). For every window deemed relevant, it’s added to a list of candidates, that are then sorted (e.g., windows with titles are more interesting for our search) and the best window is picked.
Back in FindWindowInProcessHierarchy
, if no suitable window is found, we go back one level to check the parent process, and then start our search anew, with the same criteria.

But I see that you’re stopping a few levels into the search? You’re not going to be searching forever until you reach a valid window handle? Is there some danger there?
I added the check mainly because I don’t expect the nesting to be too deep with MCP clients - having that check in place helps me avoid unexpected scenarios stemming from potential re-parenting or anything more complex that can mess with the traversal.
But that’s about it - voila! Is it a bit overkill for this scenario? Probably. Are there going to be edge cases? Absolutely, and you should test against them, and let me know in the comments so that I can update my code. But it works well so far!
You can browse this sample code on GitHub to see how my implementation works end-to-end.
Check out the codeDemo #
Let’s see what this looks like in practice. I tested with Visual Studio Code and Claude Desktop because these are the two MCP clients I am most immersed in lately.
Visual Studio Code #
Visual Studio Code has the following MCP server process parenting structure:

code.exe
process, which is parented to another code.exe
, which in turn is bound to explorer.exe
.And here is how my code handles it:

Smack in the middle of the window. Not bad. Let’s try with Claude Desktop next.
Claude Desktop #

claude.exe
.And here is how my code handles it with the client:

Also, smack in the middle. Not bad at all.
Conclusion #
You now have a much better point of reference on finding the right parent window than I did in my initial (somewhat lazy) exploration. If you have a better approach or suggestion, voice it in the comments on this blog post - I’d love to learn how I can reduce the number of lines of code to handle this scenario.