All skills
github avatar

/dotnet-mcp-builder

@aa01464 official
by githubgithub/awesome-copilot40k stars
5,040

Build Model Context Protocol (MCP) servers in C#/.NET against the current ModelContextProtocol 2.x NuGet packages. Helps with cases the model gets wrong without guidance — stale versions (0.x preview or 1.x-era defaults), the v2 stateless-by-default HTTP flip, the 2026-07-28 spec deprecations (roots/sampling/logging), MCP Apps and Tasks extension packages, elicitation URL mode, per-session HTTP wiring, OAuth and reverse-proxy deploy specifics, and debugging MapMcp / STDIO / Streamable-HTTP errors. Also covers STDIO and Streamable HTTP transports (SSE is deprecated), tools, prompts, resources, completions, and a basic .NET MCP client. Trigger when the user says or implies any .NET MCP server work: ModelContextProtocol, McpServerTool, MapMcp, WithStdioServerTransport, "MCP server in C#", "MCP tool in dotnet", "expose this as MCP", or names a primitive (prompt/resource/elicitation/MCP App) in a .NET context. Skip for MCP work in other languages.

Use this Skill: https://skilld.dev/gh/github/awesome-copilot/dotnet-mcp-builder

This session only. Nothing lands on disk.

referencesmcp-apps.md

≈2.5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

MCP Apps (interactive UI)

MCP Apps is the official extension that lets a tool return an interactive UI rendered in a sandboxed iframe inside the host (Claude, Claude Desktop, VS Code Copilot, Goose, Postman, MCPJam). Typical use cases: charts, dashboards, multi-step forms, 3D viewers, real-time monitors, PDF/video viewers.

Important: SDK 2.x ships a dedicated extension package, ModelContextProtocol.Extensions.Apps, with typed MCP Apps support: register with .WithMcpApps() and annotate tools with [McpAppUi(ResourceUri = "ui://...")]. It replaces the hand-rolled _meta wiring, not the ui:// resource — you still register and serve the UI resource. The APIs are marked experimental (suppress diagnostic MCPEXP003); check the package page and SDK API reference for the current surface rather than guessing beyond those names. The manual pattern below is what you need on 1.x, which has no typed layer (was tracked in csharp-sdk#1431): serve a ui:// resource and emit the right _meta on the tool.

How it works (short version)

  1. You register a resource at a ui:// URI returning an HTML bundle.
  2. You register a tool whose definition includes _meta.ui.resourceUri pointing to that URI.
  3. When the LLM calls the tool, the host fetches the UI resource and renders it in a sandboxed iframe in the chat.
  4. The HTML talks to the host over postMessage JSON-RPC (use @modelcontextprotocol/ext-apps from the bundle, or hand-roll it).
  5. The app can call back into your MCP server (any tool), update the model context, etc.

The full protocol spec is at @modelcontextprotocol/ext-apps.

Step 1: Serve the UI resource

Bundle your HTML/JS/CSS into a single string (or load from wwwroot). Serve it at a ui:// URI.

using System.ComponentModel;
using System.IO;
using System.Reflection;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;

[McpServerResourceType]
public static class ChartUiResource
{
    [McpServerResource(
        UriTemplate = "ui://charts/interactive",
        Name = "Interactive chart",
        MimeType = "text/html;profile=mcp-app")]   // see "MIME type" note below
    [Description("UI bundle for the interactive chart MCP App.")]
    public static TextResourceContents GetUi()
    {
        // Load a bundled HTML/JS file from embedded resources or wwwroot.
        var html = LoadEmbeddedString("MyMcpServer.AppUi.chart.html");

        return new TextResourceContents
        {
            Uri = "ui://charts/interactive",
            MimeType = "text/html;profile=mcp-app",
            Text = html
        };
    }

    private static string LoadEmbeddedString(string resourceName)
    {
        var asm = Assembly.GetExecutingAssembly();
        using var stream = asm.GetManifestResourceStream(resourceName)
            ?? throw new InvalidOperationException($"Missing embedded resource {resourceName}");
        using var reader = new StreamReader(stream);
        return reader.ReadToEnd();
    }
}

MIME type note: the current Apps spec (2026-01-26) uses text/html;profile=mcp-app for app HTML so hosts can distinguish UI bundles from regular text/html previews. Earlier drafts used text/html+skybridge — treat that as legacy; some older hosts may still expect it.

Step 2: Emit _meta on the tool

The C# SDK's [McpServerTool] doesn't expose _meta in the attribute today, so set it via the lower-level Tool definition. Do this once at startup:

using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Text.Json;
using System.Text.Json.Nodes;

builder.Services.Configure<McpServerOptions>(options =>
{
    options.Capabilities ??= new();
    options.Capabilities.Tools ??= new();

    // Define the tool manually so we can attach _meta.
    var visualizeTool = new Tool
    {
        Name = "visualize_data",
        Description = "Visualize the user's data as an interactive chart.",
        InputSchema = JsonDocument.Parse("""
            {
              "type": "object",
              "properties": {
                "datasetId": { "type": "string", "description": "Dataset to visualize." }
              },
              "required": ["datasetId"]
            }
            """).RootElement,
        Meta = new JsonObject
        {
            ["ui"] = new JsonObject
            {
                ["resourceUri"] = "ui://charts/interactive"
                // Optionally:
                // ["csp"] = new JsonObject { ["default-src"] = "'self' https://cdn.example.com" },
                // ["permissions"] = new JsonArray("clipboard-write")
            }
        }
    };

    // Implement the call handler that returns the data the UI will render.
    options.Capabilities.Tools.ToolCollection ??= new();
    options.Capabilities.Tools.ToolCollection.Add(McpServerTool.Create(
        async (CallToolRequestParams req, CancellationToken ct) =>
        {
            var args = req.Arguments ?? new();
            var datasetId = args["datasetId"]!.GetValue<string>();
            var data = await LoadDataset(datasetId, ct);
            return new CallToolResult
            {
                Content = [new TextContentBlock { Text = JsonSerializer.Serialize(data) }],
                StructuredContent = JsonSerializer.SerializeToNode(data)
            };
        },
        visualizeTool));
});

If you don't need full structured content, the tool can return just JSON in a text block — the UI fetches it via app.callServerTool(...) after rendering.

Backwards compatibility key

Some older hosts expect _meta["ui/resourceUri"] instead of _meta.ui.resourceUri. Set both for safety:

Meta = new JsonObject
{
    ["ui"] = new JsonObject { ["resourceUri"] = "ui://charts/interactive" },
    ["ui/resourceUri"] = "ui://charts/interactive"   // legacy
}

Step 3: The HTML bundle

A minimum viable bundle: vanilla JS using @modelcontextprotocol/ext-apps. The simplest build is a single self-contained HTML file.

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Chart</title>
    <style>body { font-family: system-ui; margin: 0; }</style>
  </head>
  <body>
    <div id="root">Loading…</div>
    <script type="module">
      import { App } from "https://esm.sh/@modelcontextprotocol/ext-apps@1.7.5";

      const app = new App();
      await app.connect();

      // Fetch the data we need from the server.
      const resp = await app.callServerTool({
        name: "visualize_data",
        arguments: { datasetId: "default" }
      });

      const data = JSON.parse(resp.content[0].text);
      document.getElementById("root").textContent =
        `Loaded ${data.points.length} data points.`;

      // Tell the model what just happened (becomes part of its context).
      await app.updateModelContext({
        content: [{ type: "text", text: "User opened the chart UI." }]
      });
    </script>
  </body>
</html>

Tip: for non-trivial UIs, build with Vite (React/Vue/Svelte/Solid — any of the official starter templates) and have the build emit a single inlined HTML you embed as a project resource.

Project layout

A pragmatic layout for an MCP App in .NET:

MyMcpServer/
├── Program.cs
├── Tools/
│   └── VisualizeDataTool.cs       # (or registered via Configure as above)
├── Resources/
│   └── ChartUiResource.cs         # serves the ui:// resource
├── AppUi/
│   ├── chart.html                 # bundled UI (Embedded Resource)
│   └── package.json + src/...     # if you build with Vite, output to chart.html
└── MyMcpServer.csproj

In the csproj:

<ItemGroup>
  <EmbeddedResource Include="AppUi\chart.html" />
</ItemGroup>

Read it via Assembly.GetManifestResourceStream("MyMcpServer.AppUi.chart.html").

Testing locally

  1. Run your MCP server (STDIO or HTTP).
  2. Use a host that supports MCP Apps — Claude Desktop or VS Code Copilot Chat are the easiest.
  3. Trigger the tool via the LLM. The UI renders inline.

For pure-UI iteration, MCP Inspector shows resource contents but does not fully render apps; for that, point Claude Desktop at your dev server.

Pitfalls

  • Wrong MIME type. Use text/html;profile=mcp-app (current spec; text/html+skybridge is a legacy draft value). Plain text/html may still work on lenient hosts but isn't future-proof.
  • CSP too tight or too loose. If your UI loads from a CDN, declare it in Meta["ui"]["csp"] on the Tool definition (this serialises to _meta.ui.csp on the wire). Otherwise the iframe sandbox blocks it.
  • Forgetting Tool.Meta on the tool. Without the Meta property containing the ui.resourceUri entry, the host treats your tool as a regular text-returning tool. The UI never appears.
  • Trying to use browser APIs outside the sandbox. No cookies, no localStorage from the parent. Use app.updateModelContext and tool calls for state.

Migrating from the manual pattern

On 2.x, ModelContextProtocol.Extensions.Apps replaces the manual Configure block: .WithMcpApps() plus [McpAppUi(ResourceUri = "ui://...")] on the tool, with the extension handling the Apps capability negotiation. You still serve the ui:// resource (with the current text/html;profile=mcp-app MIME type) and keep your HTML bundle — keep the UI HTML as embedded resources so the migration is mechanical. The APIs are experimental (MCPEXP003); consult the package docs for anything beyond this surface rather than inventing it.

Source: SKILL.md on GitHub

No alerts15d3 checks · Risk SAFE
  • Gen Agent Trust Hub15d

    The skill provides technical guidance and code references for building Model Context Protocol (MCP) servers and clients using the official C#/.NET SDK. It includes security best practices for resource access and follows standard development patterns.

  • Socket15d

    No alerts

  • Snyk15d

    Risk: LOW · No issues

Signed by skilld at aa01464. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 20 hours ago.

Activeupdated 2 months ago
  • MCP
  • dotnet
  • csharp
  • model-context-protocol
  • stdio
  • http
  • tools
  • prompts
  • resources
  • streaming

README badge

README badge for github/awesome-copilot/dotnet-mcp-builder

Builds Model Context Protocol servers in C# / .NET against the stable 1.x SDK, covering STDIO and HTTP transports, tools, prompts, resources, sampling, elicitation, MCP Apps, and debugging. Targets the specific pitfalls that cause breakage: stale preview package versions, stdout pollution in STDIO mode, stateless HTTP misconfiguration, and missing primitive registration in DI.

Generated from the current SKILL.md.

Does this skill work with preview versions of the ModelContextProtocol NuGet packages?
No. The skill targets stable 1.x packages only. Preview versions (0.3, 0.4) have breaking differences and won't compile against current samples. Always pin the latest 1.x release.
Can I use stateless HTTP transport with sampling, elicitation, or server-initiated notifications?
No. Stateless HTTP breaks those features at runtime because it cannot maintain bidirectional communication. Use stateful HTTP or STDIO if you need server-to-client capabilities.
What should I do if my STDIO server isn't working?
First check that nothing is writing to stdout — configure `LogToStandardErrorThreshold = LogLevel.Trace` and remove any `Console.WriteLine` calls, since stdout is the JSON-RPC channel. Also verify tools and prompts are registered with `.WithToolsFromAssembly()` or equivalent.
Does this skill cover building MCP servers in other languages like Python or TypeScript?
No. This skill is C#/.NET only. It skips MCP work in other languages.
Can I write a .NET program that consumes an MCP server instead of building one?
Yes. Load `references/client.md` for guidance on writing a basic .NET MCP client that calls an existing server.

Generated from the current SKILL.md. These answers refresh after source changes.