oidcOpts) =>
+{
+ var token = context.Request.Cookies["mcp_pair"];
+ if (!sessions.Validate(token))
+ return Results.Redirect("/pair");
+
+ // Prefer the tunnel public URL so mobile devices can reach the server externally
+ string? baseUrl = null;
+ var tunnels = await tunnelRegistry.ListAsync(context.RequestAborted).ConfigureAwait(false);
+ var activeTunnel = tunnels.FirstOrDefault(t => t.IsRunning && !string.IsNullOrEmpty(t.PublicUrl));
+ if (activeTunnel is not null)
+ {
+ baseUrl = activeTunnel.PublicUrl!.TrimEnd('/');
+ }
+
+ baseUrl ??= $"{context.Request.Scheme}://{context.Request.Host}";
+
+ // When a tunnel is active, point to the identity server proxy login page
+ string loginUrl;
+ var ids = idsOpts.Value;
+ var oidc = oidcOpts.Value;
+ if (activeTunnel is not null && oidc.Enabled)
+ {
+ // External Keycloak: proxy login page through the tunnel
+ var authority = oidc.Authority.TrimEnd('/');
+ if (Uri.TryCreate(authority, UriKind.Absolute, out var authorityUri))
+ {
+ loginUrl = $"{baseUrl}/auth/ui{authorityUri.AbsolutePath}";
+ }
+ else
+ {
+ loginUrl = $"{baseUrl}/pair";
+ }
+ }
+ else if (activeTunnel is not null && ids.Enabled)
+ {
+ // Embedded IdentityServer: login page is served by the MCP server itself via tunnel
+ loginUrl = $"{baseUrl}/pair";
+ }
+ else
+ {
+ loginUrl = $"{baseUrl}/pair";
+ }
+
+ var svg = PairingQrCode.GenerateSvg(loginUrl);
+ return Results.Content(svg, "image/svg+xml");
+}).ExcludeFromDescription();
+
// Seed IdentityServer defaults (admin user, roles) on first run
if (identityServerOptions is { Enabled: true, SeedDefaults: true })
{
diff --git a/src/McpServer.Support.Mcp/Web/PairingHtml.cs b/src/McpServer.Support.Mcp/Web/PairingHtml.cs
index 0fdd04b..1514a25 100644
--- a/src/McpServer.Support.Mcp/Web/PairingHtml.cs
+++ b/src/McpServer.Support.Mcp/Web/PairingHtml.cs
@@ -1,141 +1,148 @@
-using System.Net;
-
-namespace McpServer.Support.Mcp.Web;
-
-///
-/// Inline HTML templates for the /pair web login flow.
-/// Users authenticate with a configured username/password to view the server API key.
-///
-internal static class PairingHtml
-{
- /// Renders the login form. Shows an error banner when is not empty.
- public static string LoginPage(string? errorMessage = null)
- {
- var errorBanner = string.IsNullOrWhiteSpace(errorMessage)
- ? string.Empty
- : $"{WebUtility.HtmlEncode(errorMessage)}
";
-
- return $$"""
-
-
-
-
-
- MCP Server — Pair
-
-
-
-
-
🔗 MCP Server Pairing
-
Sign in to view your API key.
- {{errorBanner}}
-
-
-
-
- """;
- }
-
- /// Renders the API key display page.
- public static string KeyPage(string apiKey, string serverUrl)
- {
- return $$"""
-
-
-
-
-
- MCP Server — API Key
-
-
-
-
-
🔑 Your API Key
-
Use this key to authenticate mutating API calls.
-
- {{apiKey}}
- Copy
-
-
-
MCP Client Config
-
{
- "mcpServers": {
- "mcp-server": {
- "url": "{{serverUrl}}/mcp-transport"
- }
- }
- }
-
-
-
cURL Example
-
curl {{serverUrl}}/mcpserver/workspace \
- -H "X-Api-Key: {{apiKey}}"
-
-
Keep this key secret. It grants write access to workspace and tool endpoints.
-
-
-
- """;
- }
-
- /// Renders a page shown when pairing is not configured.
- public static string NotConfiguredPage()
- {
- return """
-
-
-
-
-
- MCP Server — Pairing Not Configured
-
-
-
-
-
⚠️ Pairing Not Configured
-
To enable the pairing page, add one or more users to
- Mcp:PairingUsers in your configuration and set
- Mcp:ApiKey to a non-empty value.
-
-
-
- """;
- }
-}
+using System.Net;
+
+namespace McpServer.Support.Mcp.Web;
+
+///
+/// Inline HTML templates for the /pair web login flow.
+/// Users authenticate with a configured username/password to view the server API key.
+///
+internal static class PairingHtml
+{
+ /// Renders the login form. Shows an error banner when is not empty.
+ public static string LoginPage(string? errorMessage = null)
+ {
+ var errorBanner = string.IsNullOrWhiteSpace(errorMessage)
+ ? string.Empty
+ : $"{WebUtility.HtmlEncode(errorMessage)}
";
+
+ return $$"""
+
+
+
+
+
+ MCP Server — Pair
+
+
+
+
+
🔗 MCP Server Pairing
+
Sign in to view your API key.
+ {{errorBanner}}
+
+
+
+
+ """;
+ }
+
+ /// Renders the API key display page.
+ public static string KeyPage(string apiKey, string serverUrl)
+ {
+ return $$"""
+
+
+
+
+
+ MCP Server — API Key
+
+
+
+
+
🔑 Your API Key
+
Scan the QR code with the MCP Server mobile app, or copy the key below.
+
+
📱 Scan to Pair
+
+
+
+ {{apiKey}}
+ Copy
+
+
+
MCP Client Config
+
{
+ "mcpServers": {
+ "mcp-server": {
+ "url": "{{serverUrl}}/mcp-transport"
+ }
+ }
+ }
+
+
+
cURL Example
+
curl {{serverUrl}}/mcpserver/workspace \
+ -H "X-Api-Key: {{apiKey}}"
+
+
Keep this key secret. It grants write access to workspace and tool endpoints.
+
+
+
+ """;
+ }
+
+ /// Renders a page shown when pairing is not configured.
+ public static string NotConfiguredPage()
+ {
+ return """
+
+
+
+
+
+ MCP Server — Pairing Not Configured
+
+
+
+
+
⚠️ Pairing Not Configured
+
To enable the pairing page, add one or more users to
+ Mcp:PairingUsers in your configuration and set
+ Mcp:ApiKey to a non-empty value.
+
+
+
+ """;
+ }
+}
diff --git a/src/McpServer.Support.Mcp/Web/PairingQrCode.cs b/src/McpServer.Support.Mcp/Web/PairingQrCode.cs
new file mode 100644
index 0000000..0b4016e
--- /dev/null
+++ b/src/McpServer.Support.Mcp/Web/PairingQrCode.cs
@@ -0,0 +1,18 @@
+using QRCoder;
+
+namespace McpServer.Support.Mcp.Web;
+
+///
+/// Generates QR code SVG images for the pairing flow.
+///
+internal static class PairingQrCode
+{
+ /// Generates an SVG string containing a QR code for the given .
+ public static string GenerateSvg(string text)
+ {
+ using var generator = new QRCodeGenerator();
+ using var data = generator.CreateQrCode(text, QRCodeGenerator.ECCLevel.M);
+ using var svg = new SvgQRCode(data);
+ return svg.GetGraphic(8);
+ }
+}
diff --git a/templates/prompt-templates.yaml b/templates/prompt-templates.yaml
index defaccb..26b4a41 100644
--- a/templates/prompt-templates.yaml
+++ b/templates/prompt-templates.yaml
@@ -216,16 +216,20 @@ templates:
\ h1{font-size:1.3rem;margin-bottom:4px;color:#111}\n .sub{font-size:.85rem;color:#666;margin-bottom:20px}\n \
\ .key-box{background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:14px 16px;font-family:'Cascadia Code','Fira\
\ Code',monospace;font-size:.95rem;word-break:break-all;margin-bottom:16px;position:relative}\n .copy-btn{position:absolute;top:8px;right:8px;background:#0969da;color:#fff;border:none;border-radius:4px;padding:4px\
- \ 10px;font-size:.8rem;cursor:pointer}\n .copy-btn:hover{background:#0860c4}\n .section{margin-bottom:20px}\n\
+ \ 10px;font-size:.8rem;cursor:pointer}\n .copy-btn:hover{background:#0860c4}\n .qr-section{text-align:center;margin-bottom:20px}\n\
+ \ .qr-section h2{font-size:1rem;margin-bottom:12px;color:#333}\n .qr-section img{display:inline-block;padding:12px;background:#fff;border:1px\
+ \ solid #d0d7de;border-radius:8px;width:256px;height:256px}\n .section{margin-bottom:20px}\n\
\ .section h2{font-size:1rem;margin-bottom:8px;color:#333}\n pre{background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:14px\
\ 16px;font-size:.82rem;overflow-x:auto;line-height:1.5}\n .warn{font-size:.8rem;color:#888;margin-top:12px}\n \n\
- \n\n \n
\U0001F511 Your API Key \n
Use this key to\
- \ authenticate mutating API calls.
\n
\n {apiKey} \n Copy \n\
- \
\n
\n
MCP Client Config \n
{\n \"mcpServers\": {\n \"\
- mcp-server\": {\n \"url\": \"{serverUrl}/mcp-transport\"\n }\n }\n} \n
\n
\n
cURL Example \n
curl {serverUrl}/mcpserver/workspace \\\n -H \"X-Api-Key: {apiKey}\" \n\
- \
\n
Keep this key secret. It grants write access to workspace and tool endpoints.
\n\
+ \n\n
\n
\U0001F511 Your API Key \n
Scan the QR code\
+ \ with the MCP Server mobile app, or copy the key below.
\n
\n
\U0001F4F1 Scan\
+ \ to Pair \n
\n
\n
\n\
+ \ {apiKey} \n\
+ \ Copy \n
\n
\n
MCP Client Config \n
{\n \"mcpServers\"\
+ : {\n \"mcp-server\": {\n \"url\": \"{serverUrl}/mcp-transport\"\n }\n }\n} \n
\n
\n
cURL Example \n
curl {serverUrl}/mcpserver/workspace \\\n -H \"X-Api-Key: {apiKey}\"\
+ \n
\n
Keep this key secret. It grants write access to workspace and tool endpoints.
\n\
\
\n\n