Skip to content

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

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

Labels

P2Moderate issues, valuable feature requestsbugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions