Skip to content

Commit 216efe7

Browse files
sharpninjaCopilot
andcommitted
Add Desktop connection dialog with host/port persistence
Desktop now shows a connection dialog on launch (like Android) instead of hardcoding localhost:7147 from appsettings.config. - DesktopConnectionPreferencesService: saves host, port, and OIDC JWT to %LOCALAPPDATA%\McpServerManager\connection.json - ConnectionWindow: Desktop-styled connection dialog with OIDC support - App.axaml.cs: shows connection dialog first, auto-connects on saved settings, persists connection on success, supports logout flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7a9dc03 commit 216efe7

4 files changed

Lines changed: 341 additions & 7 deletions

File tree

src/McpServerManager.Desktop/App.axaml.cs

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
using McpServerManager.Desktop.Services;
99
using McpServerManager.Desktop.Views;
1010
using System;
11+
using System.Diagnostics;
12+
using System.Globalization;
1113
using System.Linq;
14+
using System.Runtime.InteropServices;
1215

1316
namespace McpServerManager.Desktop;
1417

@@ -28,16 +31,109 @@ public override void OnFrameworkInitializationCompleted()
2831

2932
try
3033
{
31-
var clipboardService = new DesktopClipboardService(desktop);
32-
var vm = new MainWindowViewModel(clipboardService);
33-
var window = new MainWindow { DataContext = vm };
34-
desktop.MainWindow = window;
35-
window.Show();
36-
window.Activate();
34+
var connectionVm = new ConnectionViewModel();
35+
connectionVm.Host = "localhost";
36+
37+
connectionVm.SetExternalUrlOpener(url =>
38+
{
39+
try
40+
{
41+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
42+
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
43+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
44+
Process.Start("xdg-open", url);
45+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
46+
Process.Start("open", url);
47+
return true;
48+
}
49+
catch { return false; }
50+
});
51+
52+
connectionVm.SetOidcTokenCacheAccessors(
53+
() => DesktopConnectionPreferencesService.TryLoadOidcJwt(connectionVm.Host, connectionVm.Port, out var jwt)
54+
? jwt : null,
55+
token =>
56+
{
57+
if (string.IsNullOrWhiteSpace(token))
58+
DesktopConnectionPreferencesService.ClearOidcJwt();
59+
else
60+
DesktopConnectionPreferencesService.SaveOidcJwt(connectionVm.Host, connectionVm.Port, token);
61+
});
62+
63+
var connectionWindow = new ConnectionWindow { DataContext = connectionVm };
64+
desktop.MainWindow = connectionWindow;
65+
var persistNextConnection = true;
66+
67+
void OpenMainView(string mcpBaseUrl, string? mcpApiKey, string? bearerToken, bool persistConnection)
68+
{
69+
try
70+
{
71+
_logger.LogInformation(
72+
"Opening main Desktop view for {McpBaseUrl}. TokenPresent={HasToken}, BearerPresent={HasBearer}",
73+
mcpBaseUrl, !string.IsNullOrWhiteSpace(mcpApiKey), !string.IsNullOrWhiteSpace(bearerToken));
74+
75+
if (persistConnection && Uri.TryCreate(mcpBaseUrl, UriKind.Absolute, out var uri))
76+
{
77+
DesktopConnectionPreferencesService.Save(
78+
uri.Host,
79+
uri.Port.ToString(CultureInfo.InvariantCulture));
80+
}
81+
82+
var clipboardService = new DesktopClipboardService(desktop);
83+
var vm = new MainWindowViewModel(clipboardService, mcpBaseUrl, mcpApiKey, bearerToken);
84+
var mainWindow = new MainWindow { DataContext = vm };
85+
86+
vm.LogoutRequested += (_, _) =>
87+
{
88+
_logger.LogInformation("Logout requested; returning to connection dialog");
89+
DesktopConnectionPreferencesService.ClearOidcJwt();
90+
connectionVm.IsConnecting = false;
91+
connectionVm.ErrorMessage = "";
92+
persistNextConnection = true;
93+
var newConnectionWindow = new ConnectionWindow { DataContext = connectionVm };
94+
desktop.MainWindow = newConnectionWindow;
95+
newConnectionWindow.Show();
96+
mainWindow.Close();
97+
};
98+
99+
desktop.MainWindow = mainWindow;
100+
mainWindow.Show();
101+
mainWindow.Activate();
102+
connectionWindow.Close();
103+
}
104+
catch (Exception ex)
105+
{
106+
_logger.LogError(ex, "Connection failed");
107+
connectionVm.ErrorMessage = $"Connection failed: {ex.Message}";
108+
connectionVm.IsConnecting = false;
109+
}
110+
}
111+
112+
connectionVm.Connected += connection =>
113+
{
114+
_logger.LogInformation(
115+
"Connection dialog signaled Connected for {McpBaseUrl}. TokenPresent={HasToken}, BearerPresent={HasBearer}",
116+
connection.BaseUrl, !string.IsNullOrWhiteSpace(connection.ApiKey), !string.IsNullOrWhiteSpace(connection.BearerToken));
117+
var shouldPersist = persistNextConnection;
118+
persistNextConnection = true;
119+
OpenMainView(connection.BaseUrl, connection.ApiKey, connection.BearerToken, persistConnection: shouldPersist);
120+
};
121+
122+
connectionWindow.Show();
123+
124+
// Auto-connect using saved preferences or appsettings.config defaults
125+
if (DesktopConnectionPreferencesService.TryLoad(out var savedHost, out var savedPort))
126+
{
127+
_logger.LogInformation("Loaded saved Desktop connection {Host}:{Port}; auto-connecting", savedHost, savedPort);
128+
connectionVm.Host = savedHost;
129+
connectionVm.Port = savedPort;
130+
persistNextConnection = false;
131+
connectionVm.ConnectCommand.Execute(null);
132+
}
37133
}
38134
catch (Exception ex)
39135
{
40-
_logger.LogError(ex, "MainWindow creation failed");
136+
_logger.LogError(ex, "App initialization failed");
41137
System.IO.File.WriteAllText("crash.log", ex.ToString());
42138
}
43139
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System;
2+
using System.IO;
3+
using System.Text.Json;
4+
using McpServerManager.Core.Services;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace McpServerManager.Desktop.Services;
8+
9+
/// <summary>Persists the Desktop connect-dialog host/port for reuse on next startup.</summary>
10+
internal static class DesktopConnectionPreferencesService
11+
{
12+
private static readonly ILogger _logger = AppLogService.Instance.CreateLogger("DesktopConnectionPreferences");
13+
private const string FileName = "connection.json";
14+
15+
private sealed record ConnectionPrefs(string Host, string Port, string? OidcJwt = null,
16+
string? OidcJwtHost = null, string? OidcJwtPort = null);
17+
18+
public static bool TryLoad(out string host, out string port)
19+
{
20+
host = string.Empty;
21+
port = string.Empty;
22+
23+
try
24+
{
25+
var prefs = Read();
26+
if (prefs is null || string.IsNullOrWhiteSpace(prefs.Host) || string.IsNullOrWhiteSpace(prefs.Port))
27+
return false;
28+
29+
host = prefs.Host;
30+
port = prefs.Port;
31+
return true;
32+
}
33+
catch (Exception ex)
34+
{
35+
_logger.LogWarning(ex, "Failed to load connection preferences");
36+
return false;
37+
}
38+
}
39+
40+
public static void Save(string host, string port)
41+
{
42+
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(port))
43+
return;
44+
45+
try
46+
{
47+
var existing = Read();
48+
var prefs = existing is null
49+
? new ConnectionPrefs(host.Trim(), port.Trim())
50+
: existing with { Host = host.Trim(), Port = port.Trim() };
51+
Write(prefs);
52+
}
53+
catch (Exception ex)
54+
{
55+
_logger.LogWarning(ex, "Failed to save connection preferences");
56+
}
57+
}
58+
59+
public static bool TryLoadOidcJwt(string host, string port, out string jwtToken)
60+
{
61+
jwtToken = string.Empty;
62+
63+
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(port))
64+
return false;
65+
66+
try
67+
{
68+
var prefs = Read();
69+
if (prefs is null
70+
|| string.IsNullOrWhiteSpace(prefs.OidcJwt)
71+
|| !string.Equals(prefs.OidcJwtHost, host.Trim(), StringComparison.OrdinalIgnoreCase)
72+
|| !string.Equals(prefs.OidcJwtPort, port.Trim(), StringComparison.Ordinal))
73+
return false;
74+
75+
jwtToken = prefs.OidcJwt;
76+
return true;
77+
}
78+
catch (Exception ex)
79+
{
80+
_logger.LogWarning(ex, "Failed to load OIDC JWT from connection preferences");
81+
return false;
82+
}
83+
}
84+
85+
public static void SaveOidcJwt(string host, string port, string jwtToken)
86+
{
87+
if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(port) || string.IsNullOrWhiteSpace(jwtToken))
88+
return;
89+
90+
try
91+
{
92+
var existing = Read() ?? new ConnectionPrefs(host.Trim(), port.Trim());
93+
var prefs = existing with
94+
{
95+
OidcJwtHost = host.Trim(),
96+
OidcJwtPort = port.Trim(),
97+
OidcJwt = jwtToken.Trim()
98+
};
99+
Write(prefs);
100+
}
101+
catch (Exception ex)
102+
{
103+
_logger.LogWarning(ex, "Failed to save OIDC JWT to connection preferences");
104+
}
105+
}
106+
107+
public static void ClearOidcJwt()
108+
{
109+
try
110+
{
111+
var existing = Read();
112+
if (existing is null) return;
113+
Write(existing with { OidcJwt = null, OidcJwtHost = null, OidcJwtPort = null });
114+
}
115+
catch (Exception ex)
116+
{
117+
_logger.LogWarning(ex, "Failed to clear OIDC JWT from connection preferences");
118+
}
119+
}
120+
121+
private static string GetFilePath()
122+
{
123+
var dir = Path.Combine(
124+
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
125+
"McpServerManager");
126+
Directory.CreateDirectory(dir);
127+
return Path.Combine(dir, FileName);
128+
}
129+
130+
private static ConnectionPrefs? Read()
131+
{
132+
var path = GetFilePath();
133+
if (!File.Exists(path)) return null;
134+
var json = File.ReadAllText(path);
135+
return JsonSerializer.Deserialize<ConnectionPrefs>(json,
136+
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
137+
}
138+
139+
private static void Write(ConnectionPrefs prefs)
140+
{
141+
var path = GetFilePath();
142+
var json = JsonSerializer.Serialize(prefs, new JsonSerializerOptions { WriteIndented = true });
143+
File.WriteAllText(path, json);
144+
}
145+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<Window xmlns="https://github.com/avaloniaui"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:vm="using:McpServerManager.Core.ViewModels"
4+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6+
mc:Ignorable="d" d:DesignWidth="420" d:DesignHeight="400"
7+
x:Class="McpServerManager.Desktop.Views.ConnectionWindow"
8+
x:DataType="vm:ConnectionViewModel"
9+
Title="Connect to MCP Server"
10+
Width="420" Height="400"
11+
WindowStartupLocation="CenterScreen"
12+
CanResize="False"
13+
SystemDecorations="Full">
14+
15+
<Border Padding="24" VerticalAlignment="Center">
16+
<StackPanel Spacing="12">
17+
<TextBlock Text="Connect to MCP Server"
18+
FontSize="22"
19+
FontWeight="SemiBold"/>
20+
21+
<TextBlock Text="Host"/>
22+
<TextBox Text="{Binding Host, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
23+
Watermark="e.g. localhost"
24+
IsEnabled="{Binding !IsConnecting}"/>
25+
26+
<TextBlock Text="Port"/>
27+
<TextBox Text="{Binding Port, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"
28+
Watermark="e.g. 7147"
29+
IsEnabled="{Binding !IsConnecting}"/>
30+
31+
<TextBlock Text="{Binding ErrorMessage}"
32+
Foreground="Red" TextWrapping="Wrap"
33+
IsVisible="{Binding ErrorMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
34+
35+
<Border Padding="12"
36+
Background="{DynamicResource SystemControlBackgroundChromeMediumBrush}"
37+
CornerRadius="4"
38+
IsVisible="{Binding IsOidcSignInRequired}">
39+
<StackPanel Spacing="8">
40+
<TextBlock Text="Sign In Required"
41+
FontSize="16"
42+
FontWeight="SemiBold"/>
43+
44+
<TextBlock Text="Use the code below to authorize this device."
45+
TextWrapping="Wrap" Opacity="0.7"/>
46+
47+
<TextBlock Text="User Code"
48+
IsVisible="{Binding OidcUserCode, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
49+
<TextBox Text="{Binding OidcUserCode}"
50+
IsReadOnly="True"
51+
IsVisible="{Binding OidcUserCode, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
52+
53+
<TextBlock Text="Verification URL"
54+
IsVisible="{Binding OidcVerificationUrl, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
55+
<SelectableTextBlock Text="{Binding OidcVerificationUrl}"
56+
TextWrapping="Wrap"
57+
IsVisible="{Binding OidcVerificationUrl, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
58+
59+
<Button Content="Open Sign-In Page"
60+
Command="{Binding OpenOidcVerificationUrlCommand}"
61+
IsVisible="{Binding OidcCanOpenBrowser}"/>
62+
63+
<TextBlock Text="{Binding OidcStatusMessage}"
64+
TextWrapping="Wrap" Opacity="0.7"
65+
IsVisible="{Binding OidcStatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
66+
</StackPanel>
67+
</Border>
68+
69+
<Button Content="Connect"
70+
Command="{Binding ConnectCommand}"
71+
HorizontalAlignment="Stretch"
72+
HorizontalContentAlignment="Center"
73+
IsEnabled="{Binding !IsConnecting}"
74+
Margin="0,8,0,0"/>
75+
76+
<TextBlock Text="Connecting..."
77+
Opacity="0.7"
78+
HorizontalAlignment="Center"
79+
IsVisible="{Binding IsConnecting}"/>
80+
</StackPanel>
81+
</Border>
82+
</Window>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Avalonia.Controls;
2+
3+
namespace McpServerManager.Desktop.Views;
4+
5+
public partial class ConnectionWindow : Window
6+
{
7+
public ConnectionWindow()
8+
{
9+
InitializeComponent();
10+
}
11+
}

0 commit comments

Comments
 (0)