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
- Discover and invoke the precompiled local stdio MCP server.
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| Mechanism | Scope |
|---|---|
| demos/Demo400_LocalMcp/Server/Demo400.Server.csproj | Server and SupportTools fixtures; ServeurMcpLocal.dll |
| demos/Demo400_LocalMcp/Client/Demo400.Client.csproj | Host and LocalMcpSession; ClientAgentMcp.dll |
Run the client that owns the server
dotnet run --project demos/Demo400_LocalMcp/Client/Demo400.Client.csproj -c Release --no-buildThere 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();
Source code · Demo400 · Folder
- demos/Demo400_LocalMcp/Server/Demo400.Server.csproj
- demos/Demo400_LocalMcp/Client/Demo400.Client.csproj
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);
Source code · Demo400 · Folder
- demos/Demo400_LocalMcp/Server/Demo400.Server.csproj
- demos/Demo400_LocalMcp/Client/Demo400.Client.csproj
[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);
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());
Source code · Demo400 · Folder
- demos/Demo400_LocalMcp/Server/Demo400.Server.csproj
- demos/Demo400_LocalMcp/Client/Demo400.Client.csproj
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-restoreCheck 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.
{"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);
