From 1f14a9e88d47f467b6efc1432d16d7ab74e51136 Mon Sep 17 00:00:00 2001 From: Jon Galloway Date: Sat, 21 Mar 2026 12:41:57 -0700 Subject: [PATCH 1/6] Add MCP Apps UI support with SDK dashboard Implement MCP Apps (SEP-1865) support for the DotnetSdk tool, rendering an interactive HTML dashboard inline in VS Code chat. - Add McpAppsResources with ui:// resource returning themed HTML dashboard - Add both modern nested and legacy flat McpMeta keys on DotnetSdk tool (VS Code requires the legacy 'ui/resourceUri' flat key) - Register McpAppsResources in Program.cs - Add wire format tests validating both _meta key formats - Add doc/mcp-apps.md implementation guide --- .../Server/McpAppsMetaWireFormatTest.cs | 86 ++++ DotNetMcp/Program.cs | 1 + DotNetMcp/Resources/McpAppsResources.cs | 440 ++++++++++++++++++ .../Sdk/DotNetCliTools.Sdk.Consolidated.cs | 2 + doc/mcp-apps.md | 324 +++++++++++++ 5 files changed, 853 insertions(+) create mode 100644 DotNetMcp.Tests/Server/McpAppsMetaWireFormatTest.cs create mode 100644 DotNetMcp/Resources/McpAppsResources.cs create mode 100644 doc/mcp-apps.md diff --git a/DotNetMcp.Tests/Server/McpAppsMetaWireFormatTest.cs b/DotNetMcp.Tests/Server/McpAppsMetaWireFormatTest.cs new file mode 100644 index 0000000..a49eeff --- /dev/null +++ b/DotNetMcp.Tests/Server/McpAppsMetaWireFormatTest.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using Xunit; + +namespace DotNetMcp.Tests; + +/// +/// Verifies the wire format of _meta.ui on the dotnet_sdk tool, +/// ensuring both modern nested and legacy flat key formats are present +/// for MCP Apps compatibility with VS Code. +/// +public class McpAppsMetaWireFormatTest : IAsyncLifetime +{ + private McpClient? _client; + + public async ValueTask InitializeAsync() + { + var serverPath = GetServerExecutablePath(); + var transportOptions = new StdioClientTransportOptions + { + Command = serverPath.command, + Arguments = serverPath.args, + Name = "dotnet-mcp-test", + }; + + _client = await McpClient.CreateAsync( + new StdioClientTransport(transportOptions)); + } + + public async ValueTask DisposeAsync() + { + if (_client != null) + await _client.DisposeAsync(); + } + + [Fact] + public async Task DotnetSdk_Meta_Ui_HasModernNestedFormat() + { + Assert.NotNull(_client); + + var meta = await GetSdkToolMeta(); + + Assert.True(meta.TryGetProperty("ui", out var ui), "_meta should contain 'ui' key"); + Assert.Equal(JsonValueKind.Object, ui.ValueKind); + Assert.True(ui.TryGetProperty("resourceUri", out var resourceUri), + "_meta.ui should contain 'resourceUri'"); + Assert.Equal("ui://dotnet-mcp/sdk-dashboard", resourceUri.GetString()); + } + + [Fact] + public async Task DotnetSdk_Meta_Ui_HasLegacyFlatKey() + { + Assert.NotNull(_client); + + var meta = await GetSdkToolMeta(); + + // Legacy flat key required for VS Code compatibility + Assert.True(meta.TryGetProperty("ui/resourceUri", out var legacyUri), + "_meta should contain legacy 'ui/resourceUri' flat key (required for VS Code)"); + Assert.Equal("ui://dotnet-mcp/sdk-dashboard", legacyUri.GetString()); + } + + private async Task GetSdkToolMeta() + { + var tools = await _client!.ListToolsAsync( + cancellationToken: TestContext.Current.CancellationToken); + var sdkTool = tools.FirstOrDefault(t => t.Name == "dotnet_sdk"); + Assert.NotNull(sdkTool); + + var json = JsonSerializer.Serialize(sdkTool.ProtocolTool); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + Assert.True(root.TryGetProperty("_meta", out var meta), + "dotnet_sdk tool should have _meta field"); + return meta.Clone(); + } + + private static (string command, string[] args) GetServerExecutablePath() + { + var csproj = Path.GetFullPath( + Path.Join(AppContext.BaseDirectory, "..", "..", "..", "..", "DotNetMcp", "DotNetMcp.csproj")); + return ("dotnet", ["run", "--project", csproj, "--no-build"]); + } +} \ No newline at end of file diff --git a/DotNetMcp/Program.cs b/DotNetMcp/Program.cs index c5877b1..226cd72 100644 --- a/DotNetMcp/Program.cs +++ b/DotNetMcp/Program.cs @@ -73,6 +73,7 @@ .WithStdioServerTransport() .WithTools() .WithResources() + .WithResources() .WithPrompts() .WithSubscribeToResourcesHandler((context, ct) => { diff --git a/DotNetMcp/Resources/McpAppsResources.cs b/DotNetMcp/Resources/McpAppsResources.cs new file mode 100644 index 0000000..38eac5d --- /dev/null +++ b/DotNetMcp/Resources/McpAppsResources.cs @@ -0,0 +1,440 @@ +using System.Text.Json.Nodes; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace DotNetMcp; + +/// +/// MCP Apps UI resources (SEP-1865). +/// Provides interactive HTML views rendered inline in hosts that support +/// the io.modelcontextprotocol/ui extension. +/// +[McpServerResourceType] +public sealed class McpAppsResources +{ + [McpServerResource( + UriTemplate = "ui://dotnet-mcp/sdk-dashboard", + Name = "sdk_dashboard_ui", + MimeType = "text/html;profile=mcp-app")] + [McpMeta("ui", JsonValue = """{"prefersBorder": true}""")] + public static ResourceContents GetSdkDashboardUI() => new TextResourceContents + { + Uri = "ui://dotnet-mcp/sdk-dashboard", + MimeType = "text/html;profile=mcp-app", + Text = SdkDashboardHtml, + Meta = new JsonObject + { + ["ui"] = new JsonObject { ["prefersBorder"] = true } + } + }; + + /// + /// Interactive HTML dashboard for .NET SDK information. + /// Renders installed SDKs, runtimes, and framework info in a styled table + /// with host-aware theming via CSS variables from SEP-1865. + /// + private const string SdkDashboardHtml = """ + + + + + +.NET SDK Dashboard + + + +
+ + +
+ +
+

⚙️ Installed SDKs

+
Loading SDK data…
+
+ +
+

📦 Installed Runtimes

+
Loading runtime data…
+
+ +
+ + + + +"""; +} diff --git a/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs b/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs index 52b0115..6b85968 100644 --- a/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs +++ b/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs @@ -39,6 +39,8 @@ public sealed partial class DotNetCliTools [McpMeta("commonlyUsed", true)] [McpMeta("consolidatedTool", true)] [McpMeta("actions", JsonValue = """["Version","Info","ListSdks","ListRuntimes","ListTemplates","SearchTemplates","TemplateInfo","ClearTemplateCache","ListTemplatePacks","InstallTemplatePack","UninstallTemplatePack","FrameworkInfo","CacheMetrics","ConfigureGlobalJson"]""")] + [McpMeta("ui", JsonValue = """{"resourceUri": "ui://dotnet-mcp/sdk-dashboard"}""")] + [McpMeta("ui/resourceUri", "ui://dotnet-mcp/sdk-dashboard")] public async partial Task DotnetSdk( DotnetSdkAction action, string? searchTerm = null, diff --git a/doc/mcp-apps.md b/doc/mcp-apps.md new file mode 100644 index 0000000..247922a --- /dev/null +++ b/doc/mcp-apps.md @@ -0,0 +1,324 @@ +# MCP Apps Integration Guide + +## Overview + +[MCP Apps](https://github.com/modelcontextprotocol/ext-apps) (SEP-1865) is an extension to the Model Context Protocol that allows MCP servers to surface interactive HTML UIs inline within host applications like VS Code. When a tool with UI metadata is called, the host renders the linked HTML resource in an embedded webview alongside the tool's text output. + +This document covers how to implement MCP Apps in a **C# / .NET MCP server** using the `ModelContextProtocol` NuGet SDK, with VS Code as the host. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ VS Code (Host) │ +│ ┌──────────────┐ ┌──────────────────────────────┐ │ +│ │ MCP Client │────▶│ MCP Server (stdio) │ │ +│ │ │◀────│ • tools/list │ │ +│ └──────┬───────┘ │ • tools/call │ │ +│ │ │ • resources/read │ │ +│ ┌──────▼───────┐ └──────────────────────────────┘ │ +│ │ Webview │ │ +│ │ (iframe) │ ◀─── postMessage (JSON-RPC) ───▶ │ +│ │ HTML App │ HOST (VS Code) │ +│ └──────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +Key insight: the HTML app communicates with the **host** (VS Code) via `postMessage`, not directly with the MCP server. The host proxies `tools/call` and `resources/read` requests to the server on behalf of the app. The MCP server only needs to: + +1. Declare a `ui://` resource that returns HTML +2. Add `_meta` linking a tool to that resource +3. Return the HTML via `resources/read` + +## Implementation + +### 1. Resource Class + +Create a resource class that returns HTML with the `text/html;profile=mcp-app` MIME type: + +```csharp +using System.Text.Json.Nodes; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace DotNetMcp; + +[McpServerResourceType] +public sealed class McpAppsResources +{ + [McpServerResource( + UriTemplate = "ui://dotnet-mcp/sdk-dashboard", + Name = "sdk_dashboard_ui", + MimeType = "text/html;profile=mcp-app")] + public static ResourceContents GetSdkDashboardUI() => new TextResourceContents + { + Uri = "ui://dotnet-mcp/sdk-dashboard", + MimeType = "text/html;profile=mcp-app", + Text = "...", + Meta = new JsonObject + { + ["ui"] = new JsonObject { ["prefersBorder"] = true } + } + }; +} +``` + +Requirements: +- **URI scheme**: Must use `ui://` +- **MIME type**: Must be `text/html;profile=mcp-app` +- **Return type**: `TextResourceContents` (not `string`) with `Uri`, `MimeType`, `Text`, and optional `Meta` +- **`Meta.ui`**: Optional rendering hints like `prefersBorder` + +### 2. Tool Metadata Linking + +Link a tool to the UI resource using `[McpMeta]` attributes: + +```csharp +[McpServerTool(Title = ".NET SDK & Templates")] +[McpMeta("ui", JsonValue = """{"resourceUri": "ui://dotnet-mcp/sdk-dashboard"}""")] +[McpMeta("ui/resourceUri", "ui://dotnet-mcp/sdk-dashboard")] +public async partial Task DotnetSdk(...) +``` + +> **Critical**: You must include **both** metadata keys. See [Common Pitfalls](#the-legacy-flat-key-is-required) below. + +### 3. Register in Program.cs + +```csharp +builder.Services + .AddMcpServer() + .WithResources() + // ... other registrations +``` + +### 4. HTML App Structure + +The HTML returned by the resource must implement the MCP Apps client protocol: + +```html + + + + + + + +
Loading...
+ + + + +``` + +Key app behaviors: +- **`ui/initialize`**: Handshake with the host; receives theme context +- **`ui/notifications/initialized`**: Confirms the app is ready +- **`tools/call`**: Call MCP server tools (proxied by the host) +- **`ui/notifications/size-changed`**: Report content size for iframe resizing +- **`ui/message`**: Send messages to the chat conversation +- **Host notifications**: `tool-input`, `tool-result`, `host-context-changed` + +## Common Pitfalls + +### The Legacy Flat Key Is Required + +The ext-apps spec defines two ways to link a tool to a UI resource: + +| Format | Key | Example | +|--------|-----|---------| +| **Modern (nested)** | `_meta.ui.resourceUri` | `{ "ui": { "resourceUri": "ui://..." } }` | +| **Legacy (flat)** | `_meta["ui/resourceUri"]` | `{ "ui/resourceUri": "ui://..." }` | + +The spec says hosts "must check both formats." In practice, **VS Code currently only checks the legacy flat key**. The TypeScript `registerAppTool` helper from `@modelcontextprotocol/ext-apps` automatically emits both, which is why TypeScript servers work out of the box. + +In C#, you must add both explicitly: + +```csharp +// Modern nested format (for spec compliance and future hosts) +[McpMeta("ui", JsonValue = """{"resourceUri": "ui://dotnet-mcp/sdk-dashboard"}""")] +// Legacy flat key (required for VS Code to detect the UI) +[McpMeta("ui/resourceUri", "ui://dotnet-mcp/sdk-dashboard")] +``` + +Without the flat key, VS Code will: +- Successfully fetch the resource via `resources/read` +- Execute the tool normally +- **Never create a webview** (zero MCP App console messages) + +### Return TextResourceContents, Not String + +The resource handler must return `TextResourceContents` with explicit properties: + +```csharp +// CORRECT +public static ResourceContents GetUI() => new TextResourceContents +{ + Uri = "ui://dotnet-mcp/sdk-dashboard", + MimeType = "text/html;profile=mcp-app", + Text = htmlString, + Meta = new JsonObject { ["ui"] = new JsonObject { ["prefersBorder"] = true } } +}; +``` + +Returning a plain `string` from the resource handler will not include the required MIME type in the wire format. + +### VS Code Setting Must Be Enabled + +MCP Apps requires `chat.mcp.apps.enabled` to be `true` in VS Code settings. This is a preview feature in VS Code Insiders. + +### Tool Name Casing in tools/call from the App + +When your HTML app calls `tools/call` via the host proxy, use the exact tool name as it appears in `tools/list`. The C# MCP SDK lowercases and snake_cases method names (e.g., `DotnetSdk` → `dotnet_sdk`). + +## Wire Format Reference + +A working `tools/list` response for a tool with MCP Apps UI looks like: + +```json +{ + "name": "dotnet_sdk", + "title": ".NET SDK & Templates", + "inputSchema": { ... }, + "_meta": { + "ui": { "resourceUri": "ui://dotnet-mcp/sdk-dashboard" }, + "ui/resourceUri": "ui://dotnet-mcp/sdk-dashboard", + "category": "sdk", + "priority": 9 + } +} +``` + +The `resources/read` response for the UI resource: + +```json +{ + "contents": [{ + "uri": "ui://dotnet-mcp/sdk-dashboard", + "mimeType": "text/html;profile=mcp-app", + "text": "...", + "_meta": { + "ui": { "prefersBorder": true } + } + }] +} +``` + +## Theming + +MCP Apps defines CSS custom properties that the host injects. Use these in your HTML for automatic light/dark theme support: + +| Variable | Purpose | +|----------|---------| +| `--color-background-primary` | Main background | +| `--color-background-secondary` | Card/table header background | +| `--color-text-primary` | Main text color | +| `--color-text-secondary` | Muted text | +| `--color-border-primary` | Borders | +| `--font-sans` | UI font family | +| `--font-mono` | Code font family | + +Use `color-scheme: light dark` and `light-dark()` for CSS fallback values when the host hasn't provided variables yet. + +## Testing + +### Wire Format Test + +Verify the `_meta` serialization is correct with a test that connects to the server and inspects the `tools/list` response: + +```csharp +[Fact] +public async Task DotnetSdk_Meta_Ui_ShouldHaveBothFormats() +{ + var tools = await _client.ListToolsAsync(); + var sdkTool = tools.First(t => t.Name == "dotnet_sdk"); + var json = JsonSerializer.Serialize(sdkTool.ProtocolTool, ...); + using var doc = JsonDocument.Parse(json); + var meta = doc.RootElement.GetProperty("_meta"); + + // Modern nested format + var ui = meta.GetProperty("ui"); + Assert.Equal(JsonValueKind.Object, ui.ValueKind); + Assert.Equal("ui://dotnet-mcp/sdk-dashboard", + ui.GetProperty("resourceUri").GetString()); + + // Legacy flat key + Assert.Equal("ui://dotnet-mcp/sdk-dashboard", + meta.GetProperty("ui/resourceUri").GetString()); +} +``` + +### Manual Testing + +1. Build the project +2. Register in VS Code's `mcp.json` +3. Enable `chat.mcp.apps.enabled` in VS Code Insiders settings +4. Call the tool from Copilot Chat — the UI should render inline + +## References + +- [MCP Apps (ext-apps) repository](https://github.com/modelcontextprotocol/ext-apps) +- [SEP-1865 specification](https://github.com/anthropics/anthropic-cookbook/blob/main/misc/sep-1865-mcp-apps.md) +- [VS Code MCP Apps docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_mcp-server-apps) +- [basic-vue reference server](https://www.npmjs.com/package/@anthropic-ai/mcp-server-basic-vue) — minimal working TypeScript example From ffb161a8331a78c0293f95f3f94a314cf7725492 Mon Sep 17 00:00:00 2001 From: Jon Galloway Date: Sat, 21 Mar 2026 13:13:09 -0700 Subject: [PATCH 2/6] fix: update argument naming and enhance server executable path retrieval in tests --- .../Server/McpAppsMetaWireFormatTest.cs | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/DotNetMcp.Tests/Server/McpAppsMetaWireFormatTest.cs b/DotNetMcp.Tests/Server/McpAppsMetaWireFormatTest.cs index a49eeff..98a45ee 100644 --- a/DotNetMcp.Tests/Server/McpAppsMetaWireFormatTest.cs +++ b/DotNetMcp.Tests/Server/McpAppsMetaWireFormatTest.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using System.Text.Json; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; @@ -20,7 +21,7 @@ public async ValueTask InitializeAsync() var transportOptions = new StdioClientTransportOptions { Command = serverPath.command, - Arguments = serverPath.args, + Arguments = serverPath.arguments, Name = "dotnet-mcp-test", }; @@ -77,10 +78,39 @@ private async Task GetSdkToolMeta() return meta.Clone(); } - private static (string command, string[] args) GetServerExecutablePath() + /// + /// Finds the pre-built DotNetMcp server binary, matching the same + /// configuration/framework as the test project to work in CI. + /// + private static (string command, string[] arguments) GetServerExecutablePath() { - var csproj = Path.GetFullPath( - Path.Join(AppContext.BaseDirectory, "..", "..", "..", "..", "DotNetMcp", "DotNetMcp.csproj")); - return ("dotnet", ["run", "--project", csproj, "--no-build"]); + var testBinaryDir = AppContext.BaseDirectory; + var configuration = Path.GetFileName(Path.GetDirectoryName(testBinaryDir.TrimEnd(Path.DirectorySeparatorChar))!); + var targetFramework = Path.GetFileName(testBinaryDir.TrimEnd(Path.DirectorySeparatorChar)); + + var serverBinaryDir = Path.GetFullPath( + Path.Join(testBinaryDir, "..", "..", "..", "..", "DotNetMcp", "bin", configuration, targetFramework)); + + if (!Directory.Exists(serverBinaryDir)) + { + throw new DirectoryNotFoundException( + $"Server binary directory not found at: {serverBinaryDir}. " + + $"Make sure DotNetMcp project is built before running tests."); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var exePath = Path.Join(serverBinaryDir, "DotNetMcp.exe"); + if (!File.Exists(exePath)) + throw new FileNotFoundException($"Server executable not found at: {exePath}"); + return (exePath, Array.Empty()); + } + else + { + var dllPath = Path.Join(serverBinaryDir, "DotNetMcp.dll"); + if (!File.Exists(dllPath)) + throw new FileNotFoundException($"Server assembly not found at: {dllPath}"); + return ("dotnet", new[] { dllPath }); + } } } \ No newline at end of file From b248a6f799911d8fd2e1857e8118a648b5ea75ea Mon Sep 17 00:00:00 2001 From: Jon Galloway Date: Sat, 21 Mar 2026 13:32:47 -0700 Subject: [PATCH 3/6] fix: set working directory for MTP runner when project path is provided --- .../Cli/DotNetCliTools.Project.Consolidated.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/DotNetMcp/Tools/Cli/DotNetCliTools.Project.Consolidated.cs b/DotNetMcp/Tools/Cli/DotNetCliTools.Project.Consolidated.cs index b18db80..1cefdc2 100644 --- a/DotNetMcp/Tools/Cli/DotNetCliTools.Project.Consolidated.cs +++ b/DotNetMcp/Tools/Cli/DotNetCliTools.Project.Consolidated.cs @@ -1181,6 +1181,21 @@ internal async Task DotnetProjectTest( selectionSource = detectionSource; } + // When MTP is detected and a project path is provided, ensure the working + // directory is the project's directory so that `dotnet test` also walks up and + // discovers global.json with the MTP runner config. Without this, the CLI may + // fall back to VSTest internally and choke on the `--project` flag (MSB1001). + if (effectiveRunner == TestRunner.MicrosoftTestingPlatform + && !string.IsNullOrEmpty(project) + && string.IsNullOrEmpty(DotNetCommandExecutor.WorkingDirectoryOverride.Value)) + { + var projectDir = Path.GetDirectoryName(Path.GetFullPath(project)); + if (!string.IsNullOrEmpty(projectDir)) + { + DotNetCommandExecutor.WorkingDirectoryOverride.Value = projectDir; + } + } + // Build the command var args = new StringBuilder("test"); From 0ecd1cae0aacdc56d467033916634b4beb9f0771 Mon Sep 17 00:00:00 2001 From: Jon Galloway Date: Sat, 21 Mar 2026 13:53:23 -0700 Subject: [PATCH 4/6] fix: enhance SDK dashboard UI hint in tool result display --- DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs b/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs index 6b85968..95feca7 100644 --- a/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs +++ b/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs @@ -99,7 +99,13 @@ public async partial Task DotnetSdk( _ => null }; - return StructuredContentHelper.ToCallToolResult(textResult, structured); + // When structured content is available, the SDK dashboard UI renders it visually. + // Add a hint so the LLM avoids repeating the same data in prose. + var displayText = structured != null + ? $"[This data is displayed in the SDK dashboard UI. Summarize briefly or refer the user to it rather than repeating all details.]\n\n{textResult}" + : textResult; + + return StructuredContentHelper.ToCallToolResult(displayText, structured); } private async Task HandleSearchTemplatesAction(string? searchTerm, bool forceReload) From 0ce06640c552f5417419b4a46e8b61f281986f83 Mon Sep 17 00:00:00 2001 From: Jon Galloway Date: Sat, 21 Mar 2026 14:03:07 -0700 Subject: [PATCH 5/6] fix: enhance SDK dashboard UI by including runtime data in ListSdks response --- DotNetMcp/Resources/McpAppsResources.cs | 4 ++ .../Sdk/DotNetCliTools.Sdk.Consolidated.cs | 61 ++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/DotNetMcp/Resources/McpAppsResources.cs b/DotNetMcp/Resources/McpAppsResources.cs index 38eac5d..422a61a 100644 --- a/DotNetMcp/Resources/McpAppsResources.cs +++ b/DotNetMcp/Resources/McpAppsResources.cs @@ -299,6 +299,10 @@ function renderFromToolResult(params) { // DotnetSdk Version/ListSdks/ListRuntimes structured content if (structured.sdks) renderSdksFromStructured(structured); if (structured.runtimes) renderRuntimesFromStructured(structured); + else if (structured.sdks) { + // ListSdks was called but runtimes weren't included — show a helpful message + document.getElementById('runtimes').innerHTML = '
Runtime data not available from this action. Click Refresh to load.
'; + } if (structured.version) { setStatus('SDK version: ' + structured.version); } diff --git a/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs b/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs index 95feca7..66ce407 100644 --- a/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs +++ b/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs @@ -90,11 +90,14 @@ public async partial Task DotnetSdk( }; }); - // Add structured content for key actions + // Add structured content for key actions. + // For ListSdks, also fetch runtimes so the dashboard UI can render both + // from a single tool-result notification (tools/call from MCP App iframes + // is not reliably supported by all hosts). object? structured = action switch { DotnetSdkAction.Version => BuildVersionStructuredContent(textResult), - DotnetSdkAction.ListSdks => BuildListSdksStructuredContent(textResult), + DotnetSdkAction.ListSdks => await BuildListSdksWithRuntimesStructuredContentAsync(textResult), DotnetSdkAction.ListRuntimes => BuildListRuntimesStructuredContent(textResult), _ => null }; @@ -563,6 +566,60 @@ internal async Task DotnetRuntimeList() return new { sdks }; } + /// + /// Build structured content for ListSdks that also includes runtime data. + /// The SDK dashboard UI needs both to render fully from a single tool-result notification. + /// + private async Task BuildListSdksWithRuntimesStructuredContentAsync(string sdkTextResult) + { + var sdkContent = BuildListSdksStructuredContent(sdkTextResult); + + // Fetch runtimes in the background for the dashboard + try + { + var runtimeTextResult = await ExecuteDotNetCommand("--list-runtimes"); + var runtimeContent = BuildListRuntimesStructuredContent(runtimeTextResult); + + // Merge both into a single object the dashboard can consume + var sdkLines = sdkTextResult.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var sdks = sdkLines + .Where(l => !l.StartsWith("Exit Code:", StringComparison.OrdinalIgnoreCase) + && !l.StartsWith("Error", StringComparison.OrdinalIgnoreCase) + && l.TrimStart().Length > 0 && char.IsDigit(l.TrimStart()[0])) + .Select(l => + { + var parts = l.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + var ver = parts.Length > 0 ? parts[0] : l.Trim(); + var path = parts.Length > 1 ? parts[1].Trim('[', ']', ' ') : null; + return new { version = ver, path }; + }) + .ToArray(); + + var runtimeLines = runtimeTextResult.Split('\n', StringSplitOptions.RemoveEmptyEntries); + var runtimes = runtimeLines + .Where(l => !l.StartsWith("Exit Code:", StringComparison.OrdinalIgnoreCase) + && !l.StartsWith("Error", StringComparison.OrdinalIgnoreCase) + && !l.StartsWith("Command:", StringComparison.OrdinalIgnoreCase) + && l.TrimStart().Length > 0 && char.IsAsciiLetter(l.TrimStart()[0])) + .Select(l => + { + var parts = l.Trim().Split(' ', 3, StringSplitOptions.RemoveEmptyEntries); + var name = parts.Length > 0 ? parts[0] : string.Empty; + var ver = parts.Length > 1 ? parts[1] : string.Empty; + var path = parts.Length > 2 ? parts[2].Trim('[', ']', ' ') : null; + return new { name, version = ver, path }; + }) + .ToArray(); + + return new { sdks, runtimes }; + } + catch + { + // If runtime fetch fails, return SDKs only — dashboard will show what it can + return sdkContent; + } + } + private static object? BuildListRuntimesStructuredContent(string textResult) { var lines = textResult.Split('\n', StringSplitOptions.RemoveEmptyEntries); From 863abe4692fcc5cf5cab31bb38e0870c96d7a759 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:16:37 +0000 Subject: [PATCH 6/6] fix: remove unused runtimeContent variable and use specific Exception catch clause Co-authored-by: jongalloway <68539+jongalloway@users.noreply.github.com> Agent-Logs-Url: https://github.com/jongalloway/dotnet-mcp/sessions/09222802-7404-459d-a12c-088bc75feba3 --- DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs b/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs index 66ce407..073fee4 100644 --- a/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs +++ b/DotNetMcp/Tools/Sdk/DotNetCliTools.Sdk.Consolidated.cs @@ -578,7 +578,6 @@ internal async Task DotnetRuntimeList() try { var runtimeTextResult = await ExecuteDotNetCommand("--list-runtimes"); - var runtimeContent = BuildListRuntimesStructuredContent(runtimeTextResult); // Merge both into a single object the dashboard can consume var sdkLines = sdkTextResult.Split('\n', StringSplitOptions.RemoveEmptyEntries); @@ -613,7 +612,7 @@ internal async Task DotnetRuntimeList() return new { sdks, runtimes }; } - catch + catch (Exception) { // If runtime fetch fails, return SDKs only — dashboard will show what it can return sdkContent;