Skip to content

Unable to connect to MCP servers from netfx UI apps (e.g. WPF) #1638

@KirillOsenkov

Description

@KirillOsenkov

When writing a UI MCP client that doesn't have a console and targeting netfx, can't connect to MCP servers because of this line:

Console.InputEncoding = StreamClientSessionTransport.NoBomUtf8Encoding;

A workaround would be a try/catch or AllocConsole() + hide window.

An earlier fix was attempted here, but wasn't minimal:
#694

Repro:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net472</TargetFramework>
    <UseWPF>true</UseWPF>
    <LangVersion>latest</LangVersion>
    <AssemblyName>McpWpfRepro</AssemblyName>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="ModelContextProtocol" Version="1.2.0" />
  </ItemGroup>

</Project>
// Minimal repro for: ModelContextProtocol C# SDK stdio transport throws on
// .NET Framework WPF (Windows-subsystem) apps because StdioClientTransport
// sets Console.InputEncoding = new UTF8Encoding(false) around Process.Start
// (no StandardInputEncoding on net472). With no console attached, the
// encoding setter calls SetConsoleCP on an invalid handle and throws,
// failing the connect before the child process is even spawned.
//
// Run as-is (ApplyWorkaround = false) -> connect fails with an IOException
// (or NotSupportedException, depending on the platform path), surfaced in
// the window's text area.
//
// Flip ApplyWorkaround to true -> EnsureHiddenConsole calls AllocConsole
// once at startup and hides the window. The encoding setter then has a
// valid console handle to write to and the SDK proceeds to spawn the child.
//
// Background:
//   https://github.com/modelcontextprotocol/csharp-sdk/pull/694

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using ModelContextProtocol.Client;

namespace McpWpfRepro;

public static class Program
{
    // Flip to true to enable the AllocConsole + hide workaround.
    private const bool ApplyWorkaround = true;

    [STAThread]
    public static int Main()
    {
        if (ApplyWorkaround)
        {
            EnsureHiddenConsole();
        }

        var output = new TextBox
        {
            IsReadOnly = true,
            FontFamily = new FontFamily("Consolas"),
            FontSize = 12,
            TextWrapping = TextWrapping.Wrap,
            VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
            AcceptsReturn = true,
        };

        var connectButton = new Button { Content = "Connect to stdio MCP server (cmd /c exit)", Padding = new Thickness(8, 4, 8, 4), Margin = new Thickness(0, 0, 0, 6) };

        var panel = new DockPanel { LastChildFill = true, Margin = new Thickness(8) };
        DockPanel.SetDock(connectButton, Dock.Top);
        panel.Children.Add(connectButton);
        panel.Children.Add(output);

        var window = new Window
        {
            Title = "MCP stdio + WPF net472 repro (ApplyWorkaround = " + ApplyWorkaround + ")",
            Width = 820,
            Height = 460,
            Content = panel,
        };

        Append(output, "ApplyWorkaround = " + ApplyWorkaround);
        Append(output, "Click the button to attempt a stdio MCP connect.");
        Append(output, "");

        connectButton.Click += async (_, __) =>
        {
            connectButton.IsEnabled = false;
            try
            {
                await TryConnect(output).ConfigureAwait(true);
            }
            finally
            {
                connectButton.IsEnabled = true;
            }
        };

        var app = new Application();
        return app.Run(window);
    }

    private static async Task TryConnect(TextBox output)
    {
        Append(output, "--- connecting ---");
        try
        {
            var options = new StdioClientTransportOptions
            {
                Command = "cmd.exe",
                Arguments = new List<string> { "/c", "exit" },
                Name = "repro-server",
            };
            var transport = new StdioClientTransport(options);

            // Connect will fail BEFORE the child process is started because the SDK's
            // encoding setter throws on a Windows-subsystem net472 host.
            var client = await McpClient.CreateAsync(transport).ConfigureAwait(true);
            Append(output, "UNEXPECTED: connect returned without throwing. Client = " + client);
            await client.DisposeAsync().ConfigureAwait(true);
        }
        catch (Exception ex)
        {
            Append(output, "FAILED: " + ex.GetType().FullName + ": " + ex.Message);
            Append(output, "");
            Append(output, ex.ToString());
        }
    }

    private static void Append(TextBox output, string line)
    {
        output.AppendText(line + Environment.NewLine);
        output.ScrollToEnd();
    }

    // ---- AllocConsole workaround (off by default; enable via ApplyWorkaround) ----

    private static int s_consoleEnsured;

    private static void EnsureHiddenConsole()
    {
        if (Interlocked.CompareExchange(ref s_consoleEnsured, 1, 0) != 0)
        {
            return;
        }

        if (GetConsoleWindow() != IntPtr.Zero)
        {
            return;
        }

        if (!AllocConsole())
        {
            return;
        }

        var hwnd = GetConsoleWindow();
        if (hwnd != IntPtr.Zero)
        {
            ShowWindow(hwnd, SW_HIDE);
        }
    }

    private const int SW_HIDE = 0;

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool AllocConsole();

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetConsoleWindow();

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions