e-naxosMAF COURSE / COURS
Français

MICROSOFT AGENT FRAMEWORK · 0.16.0-rc.1

Demo400 — Local MCP

Discover two tools through stdio, compare Found, NotFound and errors, then distinguish direct execution from a native agent.

Objectives

Prepare both projects

Open PowerShell at the root of the complete canonical repository distribution, with .NET SDK 10.0.402 and locked packages. Keep shared and both projects. Default execution is deterministic and requires neither Ollama nor an API key. The solution build also compiles the server; start only the client afterwards.

dotnet build BaseTechnique.slnx -m:1 -c Release
Composition and paths from repository root
MechanismScope
demos/Demo400_LocalMcp/Server/Demo400.Server.csprojServer and SupportTools fixtures; ServeurMcpLocal.dll
demos/Demo400_LocalMcp/Client/Demo400.Client.csprojHost and LocalMcpSession; ClientAgentMcp.dll

Run the client that owns the server

dotnet run --project demos/Demo400_LocalMcp/Client/Demo400.Client.csproj -c Release --no-build

There is no MCP port to open. The client starts the precompiled server DLL through stdio, prints Discovered with JsonSchema, calls INC-318, INC-999 and list_priority_customers, then ends with Status=Completed, Mode=Protocol. Server stdout belongs to the protocol; the client prints its own JSON results.

var builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Trace);
builder.Services.AddMcpServer().WithStdioServerTransport().WithToolsFromAssembly();
await builder.Build().RunAsync();

demos/Demo400_LocalMcp/Server/Program.cs · L6–L10 · 9a895c94534c

Source code · Demo400 · Folder

var transport = new StdioClientTransport(new StdioClientTransportOptions
{
    Name = "LocalSupport", Command = "dotnet", Arguments = [serverPath],
    WorkingDirectory = Path.GetDirectoryName(serverPath)
});
McpClient? client = null;
try
{
    client = await McpClient.CreateAsync(transport, cancellationToken: deadline.Token);
    var tools = await client.ListToolsAsync(cancellationToken: deadline.Token);
    return new(client, tools, timeout);

demos/Demo400_LocalMcp/Client/LocalMcpSession.cs · L39–L49 · 9a895c94534c

Source code · Demo400 · Folder

[McpServerTool(Name = "get_incident_status", ReadOnly = true)]
[Description("Gets an incident such as INC-318. Returns Found or NotFound; never invents incidents.")]
public static IncidentLookup GetIncidentStatus(
    [Description("Incident ID matching INC- followed by three digits.")] string incidentId)
{
    if (incidentId is null || !IncidentIdPattern().IsMatch(incidentId))
        throw new McpException("Invalid incident ID. Expected INC- followed by three digits.");
    var incident = Incidents.FirstOrDefault(i => i.Id.Equals(incidentId, StringComparison.OrdinalIgnoreCase));
    return new(incident is null ? "NotFound" : "Found", incident);

demos/Demo400_LocalMcp/Server/SupportTools.cs · L28–L36 · 9a895c94534c

Source code · Demo400 · Folder

Compare agent mode and a missing server

dotnet run --project demos/Demo400_LocalMcp/Client/Demo400.Client.csproj -c Release --no-build -- --live

--live loads shared chat provider configuration, Ollama granite4:3b by default; API only through explicit selection. Check Calls, success flags and all three expected lookups before interpreting Text. AssistantClaimWithoutToolUseIsIncomplete and SingleLookupCannotClaimWholeTaskComplete tests show why the final sentence is insufficient.

var complete = calls.All(c => c.Succeeded)
    && calls.Any(c => c.Tool == "list_priority_customers")
    && calls.Any(c => c.Tool == "get_incident_status" && c.IncidentId == "INC-318")
    && calls.Any(c => c.Tool == "get_incident_status" && c.IncidentId == "INC-999");
return new(complete ? "Completed" : "Incomplete", response.Text, calls.ToArray());

demos/Demo400_LocalMcp/Client/LocalMcpSession.cs · L132–L136 · 9a895c94534c

Source code · Demo400 · Folder

dotnet run --project demos/Demo400_LocalMcp/Client/Demo400.Client.csproj -c Release --no-build -- --server (Join-Path $env:TEMP "maf-missing-server.dll")

With a genuinely absent path, expect Failed / FileNotFoundException and exit 2 without launching a server. For another compiled binary, supply --server and its quoted absolute path. Do not run an old Verify-Lot011.ps1 from a published command: it is archived, whereas current tests are distributed.

Observe then rediscover in a copy

dotnet test tests/LocalMcp.Tests/LocalMcp.Tests.csproj -m:1 -c Release --no-build --no-restore

Check required incidentId, Found/InProgress/Critical for INC-318, NotFound for INC-999 and three customers. Tests cover invalid and missing input, denied delete_incident, deadlines, cancellation and process termination after DisposeAsync. They use a real child server and scripted models when a model is needed.

Procedure: in a repository copy, add a read tool, rebuild the solution and restart. Record the new tool in Discovered. Then check that it is not offered to the agent without deliberately updating AllowedTools. Compare discovery, selection and execution authorization. Archives and canonical code remain the reference; perform this experiment in a copy.

Observed on 24 October 2026: first and last JSON records from an actual precompiled client run in another directory. Intermediate results were also checked. No model was called.

Observed output · 2026-10-24 · lot011-protocol
{"Discovered":[{"Name":"list_priority_customers","JsonSchema":{"type":"object","properties":{}}},{"Name":"get_incident_status","JsonSchema":{"type":"object","properties":{"incidentId":{"description":"Incident ID matching INC- followed by three digits.","type":"string"}},"required":["incidentId"]}}]}
{"Status":"Completed","Mode":"Protocol"}

Interpret the evidence

This demo changes no business data. Host policy is local and fixtures do not establish enterprise access. Direct protocol behavior can be checked without a model; a --live campaign is separate and does not establish general provider qualification. No remote MCP server, authentication or system isolation is supplied.

Read the components

public async Task<CallToolResult> CallAsync(string name, Dictionary<string, object?> arguments, CancellationToken token = default)
{
    if (!AllowedTools.Contains(name, StringComparer.Ordinal)) throw new UnauthorizedAccessException("Tool is not allowed by this host.");
    using var deadline = CancellationTokenSource.CreateLinkedTokenSource(token);
    deadline.CancelAfter(requestTimeout);
    return await client.CallToolAsync(name, arguments, cancellationToken: deadline.Token);

demos/Demo400_LocalMcp/Client/LocalMcpSession.cs · L59–L64 · 9a895c94534c

Source code · Demo400 · Folder

Prerequisites

Continue reading

Complete demo folder · README

lot011-interfaces-1 · 2026-10-24 · 9a895c94534cd5fc6f3da883e37e5f7856e588c1