Post

Tools and MCP in Microsoft Agent Framework: Connecting AI to Real Systems with C#

Connect Microsoft Agent Framework to real system data using C# functions and MCP, then pass deterministic evidence to an AI agent for risk analysis.

Tools and MCP in Microsoft Agent Framework: Connecting AI to Real Systems with C#

Connecting Microsoft Agent Framework to Real Systems with C# and MCP

In the previous article, Agents vs Workflows in Microsoft Agent Framework, we established a clear boundary:

Deterministic software owns facts and policy. AI owns reasoning where reasoning adds value.

Now we will implement that boundary.

For the AI Production Release Guardian, the system needs current operational evidence before any meaningful risk analysis can happen.

For Release 2.4, that evidence includes:

1
2
3
4
5
6
Deployment Status
Integration Test Results
Security Scan Status
Open Production Incidents
Incident Severity
Affected Service

The LLM should not generate any of this information.

Instead, the Release Guardian retrieves it from the systems that own it:

1
2
3
4
5
6
7
Deployment Evidence
        ↓
C# Application Function

Production Incidents
        ↓
MCP Tool

The collected evidence is then passed to the AI agent for:

1
2
3
RiskLevel
Summary
Recommendation

So the architecture for this article is simple:

Retrieve trusted evidence → apply deterministic policy → use AI to analyze the evidence.

We will implement this using C#, Microsoft Agent Framework, and Model Context Protocol (MCP).


🎯 What You'll Learn

  • How the Release Guardian retrieves deployment evidence through deterministic C# code.
  • How production incident data is exposed and consumed through Model Context Protocol (MCP).
  • How both sources are collected into a shared ReleaseContext.
  • How the AI agent receives that evidence and produces a structured risk analysis without owning release policy.

The Architecture for This Step

For this part of the series, we are focusing on one specific boundary:

How does the Release Guardian collect real operational evidence before asking AI to reason about it?

The workflow starts with a ReleaseContext for Release 2.4.

Deployment information is retrieved through normal C# application code, while production incident information comes from a separate capability exposed through MCP.

Only after that evidence has been collected does the workflow apply deterministic release policy and pass the result to the AI risk analysis layer.

flowchart TB

    Start["ReleaseContext<br/>Version 2.4"]

    Deployment["DeploymentCheckExecutor"]
    Tool["ReleaseTools<br/>GetDeploymentStatus()"]

    Incident["IncidentCheckExecutor"]
    MCPClient["ReleaseGuardianMcpClient"]
    MCPServer["ReleaseGuardian.McpServer"]
    MCPTool["get_open_incidents"]

    Decision["ReleaseDecisionExecutor<br/>Deterministic GO / NO-GO"]

    Risk["RiskAnalysisExecutor"]
    Agent["RiskAnalysisAgent"]

    Result["RiskAnalysis<br/>RiskLevel • Summary • Recommendation"]

    Start --> Deployment
    Deployment --> Tool
    Tool --> Incident

    Incident --> MCPClient
    MCPClient --> MCPServer
    MCPServer --> MCPTool

    MCPTool --> Decision
    Decision --> Risk
    Risk --> Agent
    Agent --> Result

The important point is not simply that these components use different technologies.

They have different responsibilities:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C# Function
    ↓
Retrieves application-owned deployment facts

MCP
    ↓
Exposes production incident data through a separate MCP capability

Deterministic Code
    ↓
Applies release policy and produces GO / NO-GO

AI Agent
    ↓
Interprets the collected evidence and produces risk analysis

The workflow therefore separates four concerns:

  • Operational facts come from the systems that own them.
  • Integration boundaries are handled through application code or MCP.
  • Release policy remains deterministic.
  • AI reasoning is used only after the evidence is available.

🧠 Architecture Insight

Facts should come from the systems that own them. The model can reason about those facts, but it should not be responsible for inventing them or enforcing deterministic release policy.


Source #1: Deployment Evidence from Deterministic C# Code

The first source of evidence is the deployment status.

In the current Release Guardian implementation, this information is provided by a normal C# method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static class ReleaseTools
{
    [Description(
        "Gets release readiness information including tests, deployment environment and security scan status. " +
        "Do not use this for production incidents.")]
    public static DeploymentStatus GetDeploymentStatus()
    {
        Console.WriteLine("TOOL CALLED: GetDeploymentStatus");

        return new DeploymentStatus
        {
            Version = "2.4",
            Environment = "Staging",
            UnitTestsPassed = true,
            FailedIntegrationTests = 1,
            SecurityScanPassed = true
        };
    }
}

For the demo, the method returns fixed operational data so that we can focus on the architecture.

In a production implementation, the same boundary could retrieve deployment evidence from Azure DevOps, GitHub Actions, a deployment API, or another CI/CD system.

The important point is that this capability remains deterministic application code.

The workflow invokes it through DeploymentCheckExecutor:

1
2
3
4
5
6
7
8
9
10
11
12
public override ValueTask<ReleaseContext> HandleAsync(
    ReleaseContext message,
    IWorkflowContext context,
    CancellationToken cancellationToken = default)
{
    Console.WriteLine("WORKFLOW: Checking deployment status...");

    message.DeploymentStatus =
        ReleaseTools.GetDeploymentStatus();

    return ValueTask.FromResult(message);
}

The result is then stored in the shared ReleaseContext and becomes part of the evidence used later in the release assessment.

At this stage, no AI reasoning is required.

The workflow is simply collecting a fact that can be obtained deterministically.


Source #2: Production Incident Evidence Through MCP

The second source of evidence is production incident information.

Unlike deployment status, this capability is exposed through Model Context Protocol (MCP).

In the Release Guardian solution, a separate MCP server defines the get_open_incidents tool:

MCP boundry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[McpServerToolType]
public static class ReleaseOperationsTools
{
    [McpServerTool(
        Name = "get_open_incidents"),
     Description(
        "Gets currently active production incidents " +
        "including severity, affected service and status.")]
    public static object GetOpenIncidents()
    {
        return new
        {
            OpenIncidents = 1,
            Severity = "High",
            Service = "Authentication API",
            Status = "Investigating"
        };
    }
}

For the demo, the MCP tool returns fixed incident data.

In a production system, the same MCP boundary could expose data from an incident-management platform, monitoring service, or internal operations API.

The MCP server is configured to communicate over stdio:

1
2
3
4
builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

On the application side, ReleaseGuardianMcpClient connects to the MCP server and invokes the tool by name:

1
2
3
4
5
CallToolResult result =
    await client.CallToolAsync(
        "get_open_incidents",
        new Dictionary<string, object?>(),
        cancellationToken: cancellationToken);

The returned MCP content is then deserialized into the application’s IncidentInfo model:

1
2
3
4
5
6
7
IncidentInfo? incidentInfo =
    JsonSerializer.Deserialize<IncidentInfo>(
        json,
        new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });

The execution path is therefore:

1
2
3
4
5
6
7
8
9
10
11
IncidentCheckExecutor
        ↓
ReleaseGuardianMcpClient
        ↓
Stdio Transport
        ↓
ReleaseGuardian.McpServer
        ↓
MCP Tool: get_open_incidents
        ↓
IncidentInfo

IncidentCheckExecutor does not need to know how the MCP server implements the capability.

It only consumes the result:

1
2
3
4
5
IncidentInfo incidents =
    await ReleaseGuardianMcpClient
        .GetOpenIncidentsAsync(cancellationToken);

message.IncidentInfo = incidents;

The incident evidence is then added to the same ReleaseContext that already contains the deployment evidence.

At this point, we have collected operational data from two different integration boundaries:

1
2
Deployment Evidence → Deterministic C# Code
Incident Evidence   → MCP

The AI still has not made any release decision or generated any operational facts.

Its role comes later, after the evidence has been collected.


One Shared Context, Multiple Sources of Evidence

The deployment and incident results are carried through the workflow using a shared ReleaseContext.

At this stage, the relevant part of the context is:

1
2
3
4
5
6
7
8
public class ReleaseContext
{
    public string Version { get; set; } = string.Empty;

    public DeploymentStatus? DeploymentStatus { get; set; }

    public IncidentInfo? IncidentInfo { get; set; }
}

As each executor completes, it enriches the same workflow context with new evidence:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ReleaseContext
      │
      ├── Version: 2.4
      │
      ├── DeploymentStatus
      │      ├── Unit Tests
      │      ├── Integration Tests
      │      ├── Security Scan
      │      └── Environment
      │
      └── IncidentInfo
             ├── Open Incidents
             ├── Severity
             ├── Affected Service
             └── Status

This gives the workflow one shared state containing evidence collected from two different integration boundaries:

1
2
Deployment Evidence → Deterministic C# Code
Incident Evidence   → MCP

No AI reasoning has been required to collect either source.

The workflow now has the facts it needs.

The next step is deciding what those facts mean for the release.


Deterministic Policy Still Comes First

Once the operational evidence has been collected, the workflow evaluates the release against explicit business rules.

This responsibility belongs to ReleaseDecisionExecutor.

For example, failed integration tests immediately produce a deterministic NO-GO decision:

1
2
3
4
5
6
7
8
9
10
if (message.DeploymentStatus.FailedIntegrationTests > 0)
{
    message.CanDeploy = false;

    message.DecisionReason =
        $"Deployment blocked because " +
        $"{message.DeploymentStatus.FailedIntegrationTests} integration tests failed.";

    return ValueTask.FromResult(message);
}

The same principle applies to active high-severity production incidents:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (message.IncidentInfo.OpenIncidents > 0 &&
    (message.IncidentInfo.Severity.Equals(
         "High",
         StringComparison.OrdinalIgnoreCase) ||
     message.IncidentInfo.Severity.Equals(
         "Critical",
         StringComparison.OrdinalIgnoreCase)))
{
    message.CanDeploy = false;

    message.DecisionReason =
        $"Deployment blocked because there is an active " +
        $"{message.IncidentInfo.Severity} severity production incident.";

    return ValueTask.FromResult(message);
}

These rules are not expressed as prompts.

They are application policy implemented directly in code.

1
2
3
4
5
6
7
Operational Evidence
        ↓
ReleaseDecisionExecutor
        ↓
Explicit Rules
        ↓
GO / NO-GO

The model does not decide whether a failed integration test should be ignored.

It does not reinterpret a High or Critical production incident as acceptable.

Those decisions remain deterministic.

The AI receives the result later as part of the evidence it analyzes.

✅ Design Principle

Use deterministic code to enforce release policy. Use the AI agent to analyze context, explain risk, identify relationships, and describe potential production impact.


AI Receives the Collected Evidence

At this point, the workflow has already completed two responsibilities:

  1. collecting operational evidence,
  2. applying the deterministic release policy.

Only then does RiskAnalysisExecutor invoke the RiskAnalysisAgent.

The executor builds the agent input directly from the current ReleaseContext:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
string prompt = $"""
    Analyze the following software release.

    Version:
    {message.Version}

    Failed Integration Tests:
    {message.DeploymentStatus?.FailedIntegrationTests}

    Unit Tests Passed:
    {message.DeploymentStatus?.UnitTestsPassed}

    Security Scan Passed:
    {message.DeploymentStatus?.SecurityScanPassed}

    Open Production Incidents:
    {message.IncidentInfo?.OpenIncidents}

    Incident Severity:
    {message.IncidentInfo?.Severity}

    Affected Service:
    {message.IncidentInfo?.Service}

    Incident Status:
    {message.IncidentInfo?.Status}

    Deterministic Release Decision:
    {(message.CanDeploy ? "GO" : "NO-GO")}

    Decision Reason:
    {message.DecisionReason}

    Explain:
    1. Overall risk level
    2. Main technical risks
    3. Potential production impact
    4. Recommendation to the release manager
    """;

Notice that the agent receives both the operational evidence and the deterministic release decision.

It is not being asked to discover whether tests failed or whether an incident exists.

Those facts have already been collected.

It is being asked to interpret what those facts mean from a risk perspective.

The agent is invoked with a typed response:

1
2
3
4
5
6
AgentResponse<RiskAnalysis> response =
    await _agent.RunAsync<RiskAnalysis>(
        prompt,
        cancellationToken: cancellationToken);

message.RiskAnalysis = response.Result;

The result is mapped into a small domain model:

1
2
3
4
5
6
7
8
public class RiskAnalysis
{
    public string RiskLevel { get; set; } = string.Empty;

    public string Summary { get; set; } = string.Empty;

    public string Recommendation { get; set; } = string.Empty;
}

This creates a clear responsibility boundary:

1
2
3
4
5
6
7
8
9
10
11
12
13
Operational Systems
        ↓
Facts

Deterministic Code
        ↓
GO / NO-GO Policy

AI Agent
        ↓
RiskLevel
Summary
Recommendation

The AI is responsible for:

1
2
3
4
Risk interpretation
Technical impact analysis
Contextual relationships
Recommendation

It is not responsible for:

1
2
3
4
Test results
Incident data
Deterministic GO / NO-GO policy
Final deployment authorization

This is the practical implementation of the architecture principle from the previous article:

Deterministic software controls what must be true. AI reasons about what the evidence may mean.


Runtime Execution Trace

The runtime output makes these boundaries visible.

Release Guardian runtime execution trace

A real execution starts by collecting deployment evidence:

So the runtime sequence matches the architecture:

1
2
3
4
5
6
7
Deployment Evidence
        ↓
Production Incident Evidence
        ↓
Deterministic Release Policy
        ↓
AI Risk Analysis

The model participates only after the workflow has established the operational facts and policy outcome.

This keeps AI reasoning inside a controlled execution process rather than making the model responsible for the entire release lifecycle.<div style="border-left: 6px solid #F39C12; background-color:#FFF4E5; padding:18px 22px; border-radius:8px; margin:28px 0;">

⚠️ Production Insight

Connecting AI to operational capabilities does not mean transferring control of the system to the model. A production-oriented architecture keeps clear boundaries between evidence retrieval, policy enforcement, AI reasoning, and authorization.

</div>


What We Added in Part 3

At this stage, the Release Guardian has clearly separated responsibilities:

ResponsibilityImplementation
Deployment evidenceDeterministic C# code
Production incident evidenceMCP tool
Shared workflow stateReleaseContext
Release policyReleaseDecisionExecutor
Contextual risk analysisRiskAnalysisAgent

The resulting execution model is:

1
2
3
4
5
6
7
Operational Evidence
        ↓
Shared ReleaseContext
        ↓
Deterministic Policy
        ↓
AI Risk Analysis

This gives the AI access to the information it needs without making it responsible for producing operational facts or controlling release policy.

The next challenge is orchestration.


Next: Building Agentic Workflows

So far, we have focused on the responsibilities of the individual components.

In Part 4, we will open ReleaseWorkflow.cs and examine how Microsoft Agent Framework coordinates those components as one controlled execution graph.

We will look at:

  • Executors
  • Edges
  • Conditional routing
  • Shared workflow state
  • Deterministic and AI-driven steps
  • Multiple execution paths

We will also examine where orchestration patterns such as Sequential, Concurrent, and Handoff fit, and when multiple specialized agents are actually justified.

The question now changes from:

How does AI get access to real system evidence?

to:

How do we coordinate deterministic software and AI reasoning inside one controlled agentic workflow?


Final Thoughts

Connecting an AI model to production systems is not primarily about giving the model more autonomy.

It is about giving it access to the right evidence while preserving clear architectural boundaries.

For the Release Guardian:

1
2
3
4
Deployment Evidence  → C#
Incident Evidence    → MCP
Release Policy       → Deterministic Code
Risk Interpretation  → AI

Each component owns a different responsibility.

The model does not invent deployment state.

It does not create production incidents.

It does not redefine release policy.

It receives trusted evidence and reasons over it.

That leads to the principle I want to carry into the next part of the series:

Connect AI to the evidence it needs, but keep control where control belongs.

References

This post is licensed under CC BY 4.0 by the author.