DEV Community

Ali Suleyman TOPUZ
Ali Suleyman TOPUZ

Posted on Originally published at topuzas.medium.com on

I Compiled Five .NET Agent Tutorials Against the Real SDKs: What Worked and What Was Invented

I read a lot of .NET agent tutorials. This month I noticed a pattern I could not shake: the prose sounded confident, the snippets looked plausible, and I had no idea which ones would survive dotnet build. Some of these posts are clearly hand-written by people who ran the code. Some read like a model wrote the code and a human wrote the intro. Most are somewhere in between.

So I stopped guessing. I took the C# snippets from this week’s posts, gave each one its own throwaway project, pinned real package versions, and let the compiler decide. Where something failed, I went to Microsoft Learn, the SDK reference docs, or the GitHub source to find the call that actually exists.

This is not a takedown. Several of these authors were more careful than I expected, and two of them explicitly warned readers that parts were sketches. I want to give credit where it is due and be precise about what broke, because “this tutorial is bad” helps nobody, and “line 14 calls a method that does not exist, use this one” helps everybody.

What I tested

  • Chatri Ngambenchawong, “Using Skills in Microsoft Agent Framework” (towardsdev.com)
  • Nithin, “Cut Agent Token Costs with Toolboxes and Tool Search in Microsoft Foundry” (c-sharpcorner.com)
  • Oshadha Thrimavithana, “Building MCP Servers in ASP.NET Core” (Medium, Part 4 of a series)
  • Jesse Liberty, four posts: “When is the LLM called”, “MCP and Microsoft Agent Framework”, “Prompts in MCP Servers”, and “Creating a Spec Kit Extension”
  • Victoria Hall, “Azure Functions Agent bindings” (Azure SDK blog)
  • Mikhail Petrusheuski, on dotnetup (Medium), which I used as part of the harness rather than as a snippet

Package versions I pinned, and where each number came from:

Microsoft.Agents.AI 1.20.0 (version used in agent-framework issue #8094)
ModelContextProtocol.AspNetCore 2.2.0 (released 2026-08-13; first version with StatefulForInitializeClients)
Azure.AI.Projects 2.1.0-beta.4 (Learn "coherent preview package set" for toolboxes)
Azure.AI.Projects.Agents 2.1.0-beta.4
.NET SDK (global.json) 10.0.100, rollForward latestFeature
Enter fullscreen mode Exit fullscreen mode

I did not use a paid model key anywhere. Everything that needed a chat client ran against a local Ollama model. More on that below.

The method: one throwaway project per snippet

The rule I set for myself was simple: the compiler is the judge, and I am not allowed to “help” the snippet in ways the reader would not know to do.

That meant three rules:

  1. Each snippet gets its own folder and its own fresh project. No shared state, no leftover usings from the previous test.
  2. If a snippet uses the author’s own domain types (a GiteaApiClient, a FinanceDbContext), I add an empty stub for that type in a separate Stubs.cs. Those are application code, not SDK claims. I never stub an SDK type. If the snippet calls mcpClient.GetAIFunctionsAsync() and that method does not exist, that is the finding.
  3. Packages come from a packages.txt per snippet. If the tutorial says x.y.z, I put x.y.z in the file and let dotnet add package fail, because that is exactly what happens to a reader who copies it.

The folder layout:

verify/
  global.json
  verify.sh
  snippets/
    01-chatri-skills-builder/
      Program.cs
      Stubs.cs
      packages.txt
    02-nithin-toolbox/
      Program.cs
      packages.txt
    03-oshadha-mcp-server/
      .web
      Program.cs
      packages.txt
    ...
Enter fullscreen mode Exit fullscreen mode

global.json pins the SDK so the results are reproducible on another machine:

{
  "sdk": {
    "version": "10.0.100",
    "rollForward": "latestFeature"
  }
}
Enter fullscreen mode Exit fullscreen mode

A packages.txt looks like this. The second column is optional: a version, the word prerelease, or nothing for latest stable.

# 03-oshadha-mcp-server/packages.txt
ModelContextProtocol.AspNetCore 2.2.0
Enter fullscreen mode Exit fullscreen mode

And here is the script. It builds every snippet, records pass or fail, and counts the distinct compiler error codes so I can see at a glance whether something is a typo (one CS1525) or a hallucinated API surface (a pile of CS1061 and CS0246).

#!/usr/bin/env bash
# verify.sh: build each snippet in its own throwaway project and record the outcome.
set -uo pipefail
ROOT="$(cd "$(dirname "$0")" && pwd)"
OUT="$ROOT/results.csv"
LOGS="$ROOT/logs"
WORK="$(mktemp -d)"
mkdir -p "$LOGS"
echo "snippet,result,details" > "$OUT"
for dir in "$ROOT"/snippets/*/; do
  name="$(basename "$dir")"
  proj="$WORK/$name"
  log="$LOGS/$name.log"
  : > "$log"
  template="console"
  [[-f "$dir/.web"]] && template="web"
  dotnet new "$template" -o "$proj" --force >>"$log" 2>&1
  rm -f "$proj/Program.cs"
  cp "$dir"/*.cs "$proj/"
  cp "$ROOT/global.json" "$proj/"
  missing=""
  while read -r pkg ver; do
    [[-z "${pkg:-}" || "$pkg" == \#*]] && continue
    if [["${ver:-}" == "prerelease"]]; then
      args=(--prerelease)
    elif [[-n "${ver:-}"]]; then
      args=(--version "$ver")
    else
      args=()
    fi
    if ! dotnet add "$proj" package "$pkg" "${args[@]}" >>"$log" 2>&1; then
      missing+="$pkg@${ver:-latest} "
    fi
  done < "$dir/packages.txt"
  if [[-n "$missing"]]; then
    echo "$name,package-restore-failed,$missing" >> "$OUT"
    continue
  fi
  if dotnet build "$proj" -nologo -v q >>"$log" 2>&1; then
    echo "$name,ok," >> "$OUT"
  else
    codes="$(grep -oE 'error CS[0-9]+' "$log" | awk '{print $2}' | sort | uniq -c \
      | awk '{printf "%s(x%s) ", $2, $1}')"
    echo "$name,build-failed,$codes" >> "$OUT"
  fi
done
column -s, -t "$OUT"
echo "Logs: $LOGS"
Enter fullscreen mode Exit fullscreen mode

Run it with:

chmod +x verify.sh
./verify.sh
Enter fullscreen mode Exit fullscreen mode

Provisioning the SDK with dotnetup

Mikhail Petrusheuski’s post on dotnetup pushed me to try it here, and it fits this kind of harness well. Run inside the repo with no channel argument, dotnetup sdk install walks up to the nearest global.json and installs a matching SDK, user-local and without admin rights. The rollForward value decides how loosely it matches. It is a public preview, and Microsoft still points production machines at the regular installers, so I only use it on throwaway boxes.

cd verify
dotnetup sdk install
dotnetup dotnet --info
Enter fullscreen mode Exit fullscreen mode

If you go this route, swap dotnet for dotnetup dotnet in the script. I kept plain dotnet in the version above so it runs on any machine that already has the SDK.

The local model: Ollama instead of a paid key

Compiling proves the API exists. It does not prove the agent loop works. For the snippets that survived, I ran a tiny smoke test against a local model so that nobody needs an Azure or OpenAI key to reproduce this.

Start Ollama natively or in Docker:

# Native
ollama pull llama3.1:8b
ollama serve

# Or Docker
docker run -d --name ollama -p 11434:11434 -v ollama:/root/.ollama ollama/ollama
docker exec ollama ollama pull llama3.1:8b
Enter fullscreen mode Exit fullscreen mode

I picked llama3.1:8b because it supports tool calling and runs on a laptop. Any Ollama model with tool support works.

OllamaSharp’s OllamaApiClient implements IChatClient, so it plugs straight into Microsoft Agent Framework:

dotnet new console -o OllamaSmoke
cd OllamaSmoke
dotnet add package Microsoft.Agents.AI --version 1.20.0
dotnet add package OllamaSharp

// Program.cs
using System.ComponentModel;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OllamaSharp;
IChatClient chat = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1:8b");
[Description("Returns the current UTC time in ISO 8601 format.")]
static string GetUtcTime() => DateTime.UtcNow.ToString("O");
AIAgent agent = chat.AsAIAgent(
    instructions: "You are terse. Use tools when they help.",
    tools: [AIFunctionFactory.Create(GetUtcTime)]);
AgentResponse response = await agent.RunAsync("What time is it in UTC right now?");
Console.WriteLine(response.Text);

dotnet run
Enter fullscreen mode Exit fullscreen mode

That is the whole “chat client” for this article. Every corrected snippet below that needs a model uses this chat variable.

Snippet by snippet

1. Chatri Ngambenchawong: Agent Framework Skills

This was the most ambitious post of the batch: file-based skills, class-based skills, inline skills, MCP-based skills, and the harness agent with approvals, all in one article, with two real sample apps behind it.

What was accurate, and I want to be loud about it: almost every type name checks out against the Microsoft Learn skills page. AgentSkillsProvider, AgentSkillsProviderBuilder, UseFileSkill, UseFileScriptRunner, UseSkill, UseMcpSkills, AgentClassSkill, AgentSkillFrontmatter, [AgentSkillScript], [AgentSkillResource], AgentInlineSkill with AddScript and AddResource, AgentFileSkillsSourceOptions, AgentMcpSkillsSourceOptions. The class-based GiteaSkill example compiled once I stubbed the author's own GiteaApiClient. The author also flagged that HarnessAgentOptions is evaluation-only behind the MAAI001 diagnostic, which is exactly the kind of warning I wish more posts included.

I initially suspected UseOptions was made up because the Learn page does not mention it. I was wrong. It shows up on the builder in agent-framework issue #8094, in a repro built against Microsoft.Agents.AI 1.20.0, with the same DisableLoadSkillApproval and DisableReadSkillResourceApproval flags. Good on the author.

What broke was in the “putting it together” section.

The stray semicolon. DisableCaching() is a real builder method (Learn lists it; caching is on by default). The problem is the line ends with .DisableCaching(); and the next line starts with .Build();. A statement cannot start with a dot, so the compiler stops with CS1525. One character.

The undeclared builder. A few lines later the post assigns to skillsBuilder, which was never declared, and the earlier chain had already called Build(). Even with the semicolon fixed, you cannot keep adding sources to something you already built.

The nested AutoApprovalRules. Inside the ToolApprovalAgentOptions initializer, the collection expression contains another AutoApprovalRules = [...] line. That is an assignment inside a collection literal, and it does not compile. The intended value is clear from context and from Learn.

UseSource and DynamicInlineSkillsSource. DynamicInlineSkillsSource is the author's own type and is not shown. UseSource is not on the Learn page, and I could not confirm it from the docs I checked, so I left that line out of the corrected version rather than guess.

SubprocessScriptRunner. The author says it is not built into the framework and had to be copied from the samples. Learn uses it and calls it demonstration-only. I treated it as sample code you copy and own, which matches both statements.

Here is the corrected composition. One builder, built once, approval options in a valid initializer:

using Microsoft.Agents.AI;
var skillsRoot = Path.Combine(AppContext.BaseDirectory, "skills");
var fileOptions = new AgentFileSkillsSourceOptions
{
    AllowedResourceExtensions = [".md", ".txt"],
    AllowedScriptExtensions = [".py"],
    SearchDepth = 3,
};
var builder = new AgentSkillsProviderBuilder()
    .UseSkill(new BudgetSkill()) // your AgentClassSkill<T>
    .UseFileSkill(skillsRoot, options: fileOptions)
    .UseFileScriptRunner(SubprocessScriptRunner.RunAsync)
    .DisableCaching();
// Optional: add MCP-hosted skills before building.
// if (mcpClient is not null)
// builder = builder.UseMcpSkills(mcpClient, new AgentMcpSkillsSourceOptions());
var skillsProvider = builder
    .UseOptions(o =>
    {
        o.DisableLoadSkillApproval = true;
        o.DisableReadSkillResourceApproval = true;
        o.DisableRunSkillScriptApproval = false;
    })
    .Build();
var toolApprovalOptions = new ToolApprovalAgentOptions
{
    AutoApprovalRules = [AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule],
};
Enter fullscreen mode Exit fullscreen mode

If you want the non-harness version, Learn shows the same approval rule attached through the agent builder:

AIAgent agent = chat.AsAIAgent(new ChatClientAgentOptions
    {
        Name = "SkillsAgent",
        ChatOptions = new() { Instructions = "You are a helpful assistant." },
        AIContextProviders = [skillsProvider],
    })
    .AsBuilder()
    .UseToolApproval(toolApprovalOptions)
    .Build();
Enter fullscreen mode Exit fullscreen mode

Verdict: compiles after fixes. Three syntax-level problems, zero invented SDK names among the ones I could check.

2. Nithin: Foundry Toolboxes and tool search

This one surprised me in the best way. The C# sample uses AIProjectClient, AgentAdministrationClient.GetAgentToolboxes(), AgentToolboxes, MCPToolboxTool, McpToolCallApprovalPolicy, GlobalMcpToolCallApprovalPolicy, ToolSearchToolboxTool, and CreateVersion. Every one of those names appears in the Learn toolbox how-to. The post also ships a Python version and a Python MCP check that the tool list collapses to tool_search and call_tool, which is a genuinely useful verification step.

Two small differences from the Learn sample are worth knowing about:

  • Learn constructs AIProjectClient positionally. The post uses named arguments endpoint: and tokenProvider:. Named arguments only compile if the parameter names match the pinned version, so this is the line to watch if you pin a different beta.
  • Learn calls CreateVersionAsync. The post calls the synchronous CreateVersion. Azure SDK clients normally ship both, so I expected this to be fine.

Also, Environment.GetEnvironmentVariable returns string?, so new Uri(endpoint) gives a nullable warning, not an error.

The most important thing the author did right was in the text: “pin your package version.” These are preview packages. Learn lists 2.1.0-beta.4 for Azure.AI.Projects, Azure.AI.Projects.Agents, and Azure.AI.Extensions.OpenAI as a set. If you float them, a renamed type in the next beta breaks you on a Tuesday.

# 02-nithin-toolbox/packages.txt
Azure.AI.Projects 2.1.0-beta.4
Azure.AI.Projects.Agents 2.1.0-beta.4
Azure.Identity
Enter fullscreen mode Exit fullscreen mode

On the token numbers: the post reports that tool search cut input tokens by more than 60% for a 50-tool toolbox and over 97% for a 1,000-tool toolbox. Those are Microsoft’s internal evaluation results, compared against a prompt-cached baseline, and the author says so and tells readers to measure their own agent. I am repeating them only with that same label: vendor numbers, not something I measured.

Verdict: compiles as pasted with the Learn package set pinned. I could not exercise the toolbox at runtime without a Foundry project, and I am not going to pretend otherwise. There is no Ollama substitute for a Foundry-hosted toolbox.

3. Oshadha Thrimavithana: MCP server in ASP.NET Core

I went into this one expecting to write “invented API.” The line that caught my eye was:

options.SessionMode = HttpServerSessionMode.Stateless;
Enter fullscreen mode Exit fullscreen mode

Every MCP server tutorial I had seen used the boolean options.Stateless = true. The 2.0 announcement post on the .NET blog only talks about Stateless (and its new default of true). So SessionMode looked like a model blending two ideas.

It is not invented. HttpServerSessionMode is a real enum in ModelContextProtocol.AspNetCore, documented in the v2 API reference with three members: Stateless, Stateful, and StatefulForInitializeClients. The stateless concepts page shows exactly the line from the article. The boolean still exists as a shorthand that sets the same underlying value. StatefulForInitializeClients arrived in 2.2.0, and the v2 docs recommend setting the mode explicitly.

So the honest result depends entirely on the version you pin:

ModelContextProtocol.AspNetCore 1.4.0 -> CS0117: no SessionMode on HttpServerTransportOptions
ModelContextProtocol.AspNetCore 2.2.0 -> compiles
Enter fullscreen mode Exit fullscreen mode

That is the real lesson from this snippet, and it cuts against my own instincts. “I have never seen this API” is not evidence that it does not exist. The SDK moved under me in July and August.

The rest of the article’s server code lines up with the SDK too: AddMcpServer(), WithHttpTransport, WithToolsFromAssembly, WithTools, WithResources, [McpServerToolType], [McpServerTool], [McpServerResourceType], AddAuthorizationFilters, WithRequestFilters with AddCallToolFilter, MapMcp("/mcp").RequireAuthorization(). The package-split explanation (Core, ModelContextProtocol, AspNetCore) matches the SDK. The later snippets reference application types that are not shown, like IDoctorAvailabilityReader and _identityResolver, which is fine in an architecture article, and a logger that is not in scope inside the filter lambda, so stub those if you build them.

The minimal server, which is what I compiled:

// Program.cs (dotnet new web)
using System.ComponentModel;
using ModelContextProtocol.AspNetCore;
using ModelContextProtocol.Server;
var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.SessionMode = HttpServerSessionMode.Stateless;
    })
    .WithTools<ClockTools>();
var app = builder.Build();
app.MapMcp("/mcp");
app.Run();
[McpServerToolType]
public sealed class ClockTools
{
    [McpServerTool, Description("Returns the current UTC time in ISO 8601 format.")]
    public static string GetUtcTime() => DateTime.UtcNow.ToString("O");
}

dotnet new web -o McpServer
cd McpServer
dotnet add package ModelContextProtocol.AspNetCore --version 2.2.0
dotnet run --urls http://localhost:5100
Enter fullscreen mode Exit fullscreen mode

Verdict: compiles as pasted on 2.2.0. Fails on 1.x. If you are on 1.x, use options.Stateless = true or upgrade.

4. Jesse Liberty: “MCP and Microsoft Agent Framework”

Credit first. Example 1, the minimal MCP client that starts a server over stdio and calls ListToolsAsync(), uses the real client API shape. The author also closes with a plain disclaimer that the post was written with AI help and may contain mistakes. I respect that, and it is more than most posts say.

The trouble starts in Example 2, labeled a sketch, and the server example:

  • mcpClient.GetAIFunctionsAsync() does not exist in the MCP C# SDK. It does not need to. ListToolsAsync() returns McpClientTool instances, and McpClientTool already derives from AIFunction. You pass them straight to the agent.
  • builder.Services.AddModelContextProtocol() does not exist. The server entry point is AddMcpServer().
  • .AddTool("CheckInvoice", ...) does not exist. The real calls are WithTools() or WithToolsFromAssembly(), with [McpServerTool] on the methods. To the author's credit, the post marks this one as pseudocode.
  • BuildYourChatClientWithFunctionInvocation() throws NotImplementedException. So even if everything else compiled, the program crashes on the first line that matters.
  • The .csproj uses Version="x.y.z" for every package, and "your Agent Framework package here" in place of a package ID. dotnet add package ... --version x.y.z fails before the compiler ever runs. I also could not find a ModelContextProtocol.Client package; the client lives in ModelContextProtocol.Core per the SDK's own package split.
  • The MapMcp() comment mentions /sse and /messages. In 2.x the legacy SSE endpoints are behind an obsolete option (diagnostic MCP9004), and Streamable HTTP is the default.

The corrected client, wired to the Ollama chat from earlier and the server from snippet 3:

dotnet new console -o McpAgent
cd McpAgent
dotnet add package ModelContextProtocol.Core --version 2.2.0
dotnet add package Microsoft.Agents.AI --version 1.20.0
dotnet add package OllamaSharp

// Program.cs
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using OllamaSharp;
IChatClient chat = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1:8b");
var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    Endpoint = new Uri("http://localhost:5100/mcp"),
    Name = "local-clock",
});
await using McpClient mcpClient = await McpClient.CreateAsync(transport);
IList<McpClientTool> tools = await mcpClient.ListToolsAsync();
Console.WriteLine($"Discovered: {string.Join(", ", tools.Select(t => t.Name))}");
AIAgent agent = chat.AsAIAgent(
    instructions: "Use the MCP tools when they help. Be brief.",
    tools: [.. tools]);
AgentResponse response = await agent.RunAsync("What is the current UTC time?");
Console.WriteLine(response.Text);
Enter fullscreen mode Exit fullscreen mode

Verdict: API does not exist for GetAIFunctionsAsync, AddModelContextProtocol, and AddTool. Example 1 is fine.

5. Jesse Liberty: “When is the LLM called”

The conceptual part of this post is good. It walks through when a model call happens versus when a tool runs locally, and the three-step comment (plan, execute tool, synthesize) is a correct mental model of a function-calling loop.

Snippet 1 creates an agent from AIProjectClient with .AsAIAgent(model:, instructions:). That is the Foundry provider path in Agent Framework and matches the shape I have seen in the framework's Foundry samples, though it needs the Foundry provider package, which the post does not name. I could not run it without a Foundry project.

Snippet 2 is where it falls apart, and again the author labels it a sketch:

  • FunctionTool with a handler: argument is not an Agent Framework type. Tools are AIFunction instances, usually from AIFunctionFactory.Create.
  • ChatClientAgentBuilder with .UseModel() and .RegisterTool() does not exist. The real builder you get from .AsBuilder() is AIAgentBuilder, and it is for middleware, not for choosing a model.
  • GetWeatherAsync and IsApproved are never defined. ToolInvocation in the middleware snippet is not a framework type either.

The middleware snippets also show signatures like IEnumerable messages and Func with a return await. I suspect the blog engine stripped the generic arguments. I am not counting that against the author; I am counting it against copy-paste, because a reader gets the stripped version.

The corrected tool example:

using System.ComponentModel;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OllamaSharp;
IChatClient chat = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1:8b");
AIFunction weather = AIFunctionFactory.Create(
    ([Description("City name, for example Seattle")] string city) =>
        $"Forecast for {city}: 14C, light rain.",
    name: "get_weather",
    description: "Get the weather forecast for a city.");
AIAgent agent = chat.AsAIAgent(
    instructions: "Answer weather questions using the get_weather tool.",
    tools: [weather]);
AgentResponse response = await agent.RunAsync("What's the weather in Seattle tomorrow?");
Console.WriteLine(response.Text);
Enter fullscreen mode Exit fullscreen mode

With Ollama I could actually watch the three phases the post describes, by putting a breakpoint inside the lambda. Model call, tool call with no model involved, model call again. The post’s mental model was right. The API it was illustrated with was not.

Verdict: API does not exist for FunctionTool and ChatClientAgentBuilder. Snippet 1 is plausible but unverified on my side.

6. Jesse Liberty: “Prompts in MCP Servers”

The MCP part of this post is close. McpClient.CreateAsync, ListPromptsAsync, and calling a prompt with arguments are the right ideas.

The model part is from an older world. OpenAIClient, ChatCompletionsOptions, ChatMessage(ChatRole.User, ...) with that shape, and GetChatCompletionsAsync are the Azure.AI.OpenAI 1.0 beta API. The 2.x package replaced that surface, so against any current version those types are gone. The post also has var transport = /* create transport per your SDK */; which is not valid C#, a switch arm that reads system" with a missing opening quote, and the hedge that SDK names "vary by SDK version." That last phrase is the one I have learned to treat as a warning light.

The corrected version drops the provider-specific client and uses IChatClient, so it works with Ollama or anything else:

using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using OllamaSharp;

IChatClient chat = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1:8b");
var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    Endpoint = new Uri("http://localhost:5100/mcp"),
});
await using McpClient mcpClient = await McpClient.CreateAsync(transport);
GetPromptResult prompt = await mcpClient.GetPromptAsync(
    "summarize-document",
    new Dictionary<string, object?> { ["url"] = "https://example.com/spec.md" });
List<ChatMessage> messages = prompt.ToChatMessages().ToList();
messages.Add(new ChatMessage(ChatRole.User, "Keep the summary under 200 words."));
ChatResponse answer = await chat.GetResponseAsync(messages);
Console.WriteLine(answer.Text);
Enter fullscreen mode Exit fullscreen mode

This one needs a server that actually exposes a summarize-document prompt. In my harness I added a [McpServerPromptType] class to the server from snippet 3 with a single [McpServerPrompt] method.

Verdict: API does not exist (the Azure.AI.OpenAI 1.x surface) plus syntax errors. The MCP prompt flow is salvageable.

7. Jesse Liberty: “Creating a Spec Kit Extension”

Not C#, so the compiler cannot judge it, but I checked the manifest against the official EXTENSION-DEVELOPMENT-GUIDE.md in the github/spec-kit repo.

The post’s extension.yml uses a top-level name (as a reverse-DNS package ID), an install.files list mapping source folders to destinations, and hooks as a list of name/file pairs. The official guide uses a different shape: schema_version, an extension block with id, name, version, and description, a requires.speckit_version range, provides.commands as a list of name/file/description, and hooks keyed by event name. Commands are named speckit... Local install is specify extension add --dev , not copying the folder into .specify/extensions/ by hand.

In fairness, the source page’s YAML lost its indentation in rendering, and the post itself tells readers to confirm placeholder names against their Spec Kit version. The C# console runner that the extension wraps is ordinary .NET and is not the problem.

A manifest in the official shape for the same idea:

schema_version: "1.0"
extension:
  id: "myfeature"
  name: "My Feature Spec Analyzer"
  version: "0.1.0"
  description: "Analyzes a spec and returns structured JSON with issues."
requires:
  speckit_version: ">=0.1.0"
provides:
  commands:
    - name: "speckit.myfeature.analyze"
      file: "commands/analyze.md"
      description: "Analyze the current spec and report issues as JSON"
hooks:
  after_tasks:
    command: "speckit.myfeature.analyze"
    optional: true
    prompt: "Run the spec analyzer?"

specify extension add --dev ./myfeature
specify extension list
Enter fullscreen mode Exit fullscreen mode

I used after_tasks because that is the event in the official example. The post hooks after planning; check the guide's event list before you pick a different one.

Verdict: does not match the official manifest schema.

8. Victoria Hall: Azure Functions Agent bindings

I want to be very clear about this one because I have already seen people ask for “the C# version.”

There is no .NET path in this post. The agent bindings preview is Python only, Python 3.13 or later, with Microsoft Agent Framework as the only supported provider. The packages are azure-functions, azurefunctions-agents-extensions-agent-framework (with [mcp] or [durable] extras), agent-framework-foundry, and azure-identity. Agents are defined in .agent.md files that hold plain Markdown instructions. The extension does not parse front matter, model settings, or tool declarations from them; model choice lives in the client factory and tools are configured in Python.

Nothing to compile. If you are a .NET shop, the closest thing today is hosting an Agent Framework agent yourself inside an isolated-worker Function. That is a different design, not a port, so I am not going to write it up as if it were the same feature.

Verdict: not applicable to .NET. No .NET path exists in the post.

The results

+----+-----------------------------------------+--------------------------+-----------------------------------------------+
| # | Snippet | Outcome | What to use instead |
+----+-----------------------------------------+--------------------------+-----------------------------------------------+
| 1 | Chatri: skills builder + approvals | Compiles after fixes | Remove stray ';' before .Build(); declare one |
| | | | builder; AutoApprovalRules = [rule] |
| 1b | Chatri: AgentClassSkill<GiteaSkill> | Compiles as pasted | (stub only the author's GiteaApiClient) |
| 2 | Nithin: Foundry toolbox + tool search | Compiles as pasted | Pin Azure.AI.Projects 2.1.0-beta.4 set |
| 3 | Oshadha: MCP server, SessionMode | Compiles as pasted | Needs MCP SDK 2.2.0; on 1.x use Stateless=true|
| 4 | Liberty: MCP + Agent Framework | API does not exist | ListToolsAsync(); AddMcpServer(); |
| | | | WithTools<T>(); real package versions |
| 4b | Liberty: minimal stdio client | Compiles as pasted | (none) |
| 5 | Liberty: FunctionTool / builder sketch | API does not exist | AIFunctionFactory.Create + AsAIAgent(tools:) |
| 6 | Liberty: MCP prompts + Azure OpenAI 1.x | API does not exist | GetPromptAsync + IChatClient |
| 7 | Liberty: Spec Kit extension.yml | Schema mismatch | schema_version / extension / provides layout |
| 8 | Hall: Functions agent bindings | N/A (Python only) | No .NET path in the post |
| - | Petrusheuski: dotnetup | Tooling, used in harness | Public preview; not for production machines |
+----+-----------------------------------------+--------------------------+-----------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Counting the main snippet per post, that is three that compile as pasted or after small fixes, three where the core API does not exist, one schema mismatch, and one that has no .NET story at all.

What strikes me is that the failures are not evenly distributed. The posts that compiled were the ones where the author had obviously run a real app (Chatri’s two sample repos) or closely followed Learn (Nithin). The posts that failed had the same symptoms over and over: names that sound like they should exist, a version string that is a placeholder, and a helper method that is never written.

Red flags I now look for before I copy anything

This is the checklist I ended up with. None of these prove a snippet is wrong. Each one means “run it before you trust it.”

  • Undefined helpers that do the hard part. BuildYourChatClientWithFunctionInvocation(), GetWeatherAsync(args), IsApproved(invocation). If the one function that would require real SDK knowledge is the one left out, the rest was probably not run either.
  • x.y.z versions or "add your package here." A real author knows which version they built against. A placeholder version means nobody restored this project.
  • throw new NotImplementedException() in the main path. Fine in a sketch that says so. Not fine in a "complete example."
  • “SDK names vary,” “consult the SDK for exact API names,” “or equivalent.” Sometimes this is honest humility. More often it is a model covering for a name it made up. Check the name.
  • Builder methods that read like English. .UseModel(), .RegisterTool(), .AddTool("name"). Real .NET builders are usually more specific and more awkward than that.
  • Two styles of the same API in one post. If snippet 1 uses AIProjectClient.AsAIAgent and snippet 2 uses a ChatClientAgentBuilder, at most one of them came from the docs.
  • APIs from a previous major version. ChatCompletionsOptions and GetChatCompletionsAsync were real once. A tutorial dated this month that uses them was not compiled this month.
  • The reverse trap: “I’ve never seen this, so it’s fake.” I nearly wrote that about HttpServerSessionMode. It shipped in the MCP C# SDK 2.x line. When an API looks unfamiliar, check the version first.

What I am still unsure about

I could not exercise the Foundry toolbox or Jesse’s AIProjectClient snippet at runtime because both need a Foundry project, and I deliberately kept this whole exercise free of paid keys. "Compiles" is a weaker claim than "works," and for those two that is all I can say.

I also only checked the API names the posts used. A snippet that compiles can still call a real method with the wrong semantics. The Ollama smoke tests catch some of that, not all.

And the SDKs keep moving. The MCP C# SDK shipped 2.0 in July and 2.2 in August. Agent Framework has been releasing fast since 1.0 in April. Some of what I marked as wrong today may become right in a release I have not seen, and something I marked as right may get renamed. That is the actual argument for the harness: not this table, but being able to rerun it next month in two minutes.

If you write .NET tutorials, here is my one ask: put the package versions you built against at the top of the post. It costs you one code block and saves every reader an afternoon.

Sources

Tags: dotnet, csharp, ai-agents, microsoft-agent-framework, model-context-protocol, mcp, ollama, azure, software-engineering, developer-tools

Top comments (0)