diff --git a/Directory.Packages.props b/Directory.Packages.props index 112fc8a..6e7172f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -74,6 +74,7 @@ + diff --git a/src/McpServer.Support.Mcp/McpServer.Support.Mcp.csproj b/src/McpServer.Support.Mcp/McpServer.Support.Mcp.csproj index 285caa2..fcaf3a8 100644 --- a/src/McpServer.Support.Mcp/McpServer.Support.Mcp.csproj +++ b/src/McpServer.Support.Mcp/McpServer.Support.Mcp.csproj @@ -49,6 +49,7 @@ + diff --git a/src/McpServer.Support.Mcp/Program.cs b/src/McpServer.Support.Mcp/Program.cs index b72481c..e9346dd 100644 --- a/src/McpServer.Support.Mcp/Program.cs +++ b/src/McpServer.Support.Mcp/Program.cs @@ -738,6 +738,55 @@ await pairingRenderer.RenderLoginPageAsync("Invalid username or password.").Conf return Results.Content(await pairingRenderer.RenderKeyPageAsync(o.ApiKey, serverUrl).ConfigureAwait(false), "text/html"); }).ExcludeFromDescription(); +app.MapGet("/pair/qr", async (HttpContext context, IOptions opts, PairingSessionService sessions, + TunnelRegistry tunnelRegistry, IOptions idsOpts, IOptions 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}} - -
-
-

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

+ QR code for pairing +
+
+ {{apiKey}} + +
+
+

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 \"QR\n
\n
\n\ + \ {apiKey}\n\ + \ \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" pairing-not-configured-page: title: Pairing Not Configured Page @@ -277,19 +281,19 @@ templates: ## Session Start (Run Once Per Session) - 1. Read this marker file for connection details and API key. - - 2. Bootstrap your preferred agent entrypoint (load docs/context/module-bootstrap.md from MCP context via context_search/context_pack; do not treat it as a local file path): - - **Preferred**: Launch `mcpserver-repl --workspace "{{workspace.WorkspacePath}}"` for YAML-over-STDIO interaction (eliminates shell quoting failures). - - **Supported**: Bootstrap PowerShell or Bash helper modules from the Tool Registry (`McpSession.psm1`, `McpTodo.psm1`) for direct script integration. - - 3. Verify the marker signature using the workspace API key in this file before contacting the server. - - 4. Call /health with a random nonce and confirm the response echoes that exact nonce. - - 5. Review recent session history and current TODOs only after signature and nonce verification succeed. - - 6. Post an initial session log turn for the session. + 1. Read this marker file for connection details and API key. + + 2. Bootstrap your preferred agent entrypoint (load docs/context/module-bootstrap.md from MCP context via context_search/context_pack; do not treat it as a local file path): + - **Preferred**: Launch `mcpserver-repl --workspace "{{workspace.WorkspacePath}}"` for YAML-over-STDIO interaction (eliminates shell quoting failures). + - **Supported**: Bootstrap PowerShell or Bash helper modules from the Tool Registry (`McpSession.psm1`, `McpTodo.psm1`) for direct script integration. + + 3. Verify the marker signature using the workspace API key in this file before contacting the server. + + 4. Call /health with a random nonce and confirm the response echoes that exact nonce. + + 5. Review recent session history and current TODOs only after signature and nonce verification succeed. + + 6. Post an initial session log turn for the session. ## Per User Message @@ -304,54 +308,54 @@ templates: ## Re-run Full Session Start Only If - The user explicitly says "Start Session". - - Signature verification fails. - - /health fails. - - /health nonce verification fails. - - Any /mcpserver/* call returns 401. - - The marker endpoint/key changes after a server restart. + - Signature verification fails. + - /health fails. + - /health nonce verification fails. + - Any /mcpserver/* call returns 401. + - The marker endpoint/key changes after a server restart. ## Rules - 0. NEVER write to TODO.yaml directly. - - 1. Generate SessionId values using `session.init` (REPL) or `New-McpSessionLogSlug -Agent -Model ` (PowerShell module), then create session logs with `session.new` (REPL) or `New-McpSessionLog -SessionId` (PowerShell). Do not handcraft SessionId values. - - 1a. Agents must identify themselves accurately in session logs. `sourceType` and the SessionId `` prefix must use the agent''s real identity in Pascal-Case. Do not use lowercase forms, placeholders, or legacy aliases. - + 0. NEVER write to TODO.yaml directly. + + 1. Generate SessionId values using `session.init` (REPL) or `New-McpSessionLogSlug -Agent -Model ` (PowerShell module), then create session logs with `session.new` (REPL) or `New-McpSessionLog -SessionId` (PowerShell). Do not handcraft SessionId values. + + 1a. Agents must identify themselves accurately in session logs. `sourceType` and the SessionId `` prefix must use the agent''s real identity in Pascal-Case. Do not use lowercase forms, placeholders, or legacy aliases. + 2. Post a new session log turn (`session.turn.add` for REPL or `Add-McpSessionTurn` for PowerShell) before starting work. Update it with results (`Response`) and actions (`Add-McpAction` / `session.action.add`) when done. Persist after each meaningful update, not just at the end. 2a. Before any compaction step, persist the current session log state. After compaction, update the session log again to record the compaction outcome and recovered context. - 3. If signature verification, the /health request, or nonce verification fails, log `MCP_UNTRUSTED`, continue without the MCP server, and do not probe additional MCP endpoints. - - 4. Marker signatures use HMAC-SHA256 with the workspace API key in this file as the verifier. Recompute the canonical payload exactly as described by the marker metadata before trusting the file. - - 5. Use `mcpserver-repl` (preferred) or PowerShell/Bash helper modules for session log and TODO operations — they handle workspace routing automatically. Do not use raw API calls. - - 6. Write decisions, requirements, and state to the session log, not just conversation. Capture rich turn detail: interpretation, response/status, actions (type/status/filePath), contextList, filesModified, designDecisions, requirementsDiscovered, blockers, and key processingDialog updates. - - 7. Follow workspace conventions in AGENTS.md and .github/copilot-instructions.md. - - 8. When you need API schemas, module examples, or compliance rules, load them from docs/context/ or use context_search. - - 9. Do not fabricate information. Acknowledge mistakes. Distinguish facts from speculation. - - 10. Prioritize correctness over speed. Do not ship code you have not verified compiles. + 3. If signature verification, the /health request, or nonce verification fails, log `MCP_UNTRUSTED`, continue without the MCP server, and do not probe additional MCP endpoints. + + 4. Marker signatures use HMAC-SHA256 with the workspace API key in this file as the verifier. Recompute the canonical payload exactly as described by the marker metadata before trusting the file. + + 5. Use `mcpserver-repl` (preferred) or PowerShell/Bash helper modules for session log and TODO operations — they handle workspace routing automatically. Do not use raw API calls. + + 6. Write decisions, requirements, and state to the session log, not just conversation. Capture rich turn detail: interpretation, response/status, actions (type/status/filePath), contextList, filesModified, designDecisions, requirementsDiscovered, blockers, and key processingDialog updates. + + 7. Follow workspace conventions in AGENTS.md and .github/copilot-instructions.md. + + 8. When you need API schemas, module examples, or compliance rules, load them from docs/context/ or use context_search. + + 9. Do not fabricate information. Acknowledge mistakes. Distinguish facts from speculation. + + 10. Prioritize correctness over speed. Do not ship code you have not verified compiles. ## Naming Conventions - - Persisted TODO IDs must be uppercase canonical ids in either --### form - (regex: ^[A-Z]+-[A-Z0-9]+-\d{3}$) or ISSUE-{number} form (regex: ^ISSUE-\d+$). - - - Valid TODO IDs: PLAN-NAMINGCONVENTIONS-001, MCP-API-042, ISSUE-17. - - - Invalid TODO IDs: plan-api-001, MCP-API-42, ISSUE-ABC, MCPAPI001. - - - Create requests may use ISSUE-NEW only when the intent is to create a new GitHub-backed TODO. - The server will create the GitHub issue immediately and persist the TODO using the canonical ISSUE-{number} id. + - Persisted TODO IDs must be uppercase canonical ids in either --### form + (regex: ^[A-Z]+-[A-Z0-9]+-\d{3}$) or ISSUE-{number} form (regex: ^ISSUE-\d+$). + + - Valid TODO IDs: PLAN-NAMINGCONVENTIONS-001, MCP-API-042, ISSUE-17. + + - Invalid TODO IDs: plan-api-001, MCP-API-42, ISSUE-ABC, MCPAPI001. + + - Create requests may use ISSUE-NEW only when the intent is to create a new GitHub-backed TODO. + The server will create the GitHub issue immediately and persist the TODO using the canonical ISSUE-{number} id. - Session IDs must use -- and start with the exact agent/source type prefix.