Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
146436 stories
·
33 followers

Build a Student Analytics Dashboard in WPF with Interactive Charts

1 Share

Build a Student Analytics Dashboard in WPF with Interactive Charts

TL;DR: Build MVVM-based WPF dashboard architecture covering data models, ViewModel projections, and grid-driven UI composition. Academic datasets, such as scores, exam results, participation rates, grade distribution, and yearly trends, are transformed using bindings, converters, and filtered ViewModel collections to drive multiple chart series with dynamic updates by year and subject.

Welcome to our Chart of the Week series!

Educators make better decisions when data tells a clear story. Raw numbers alone don’t help, but structured, visual insights do. A well-built student performance dashboard turns academic data into something educators can explore, question, and act on.

In this post, we’ll walk through building a Student Performance and Engagement Hub using Syncfusion® WPF Chart controls. The focus stays on practical UI patterns and data visualization strategies that developers can reuse in real-world education platforms. You’ll see how to visualize subject scores, engagement levels, grade distributions, and long-term trends in a way that feels intuitive and responsive.

Rather than dumping charts onto a screen, this dashboard centralizes academic indicators into a single, decision-friendly view, helping schools spot patterns early and respond faster.

The dashboard highlights the academic signals that educators and administrators care about most:

  • KPI scores: Overall subject scores.
  • Examination results: Pass, Fail, and Not Attended counts by subjects.
  • Participation rates: Engagement levels across subjects.
  • Grade distribution: A – F breakdown across the student population.
  • Semester grade trends: Performance changes over time.
  • Students per year trend: Enrollment trends across academic years.
  • Gender participation: Distribution of participation by gender.

These metrics enable educators to track trends over time, compare performance across subjects, and identify groups that may require additional support.

UI architecture overview

The dashboard follows a clean MVVM structure: a well-defined model, a reactive ViewModel, and a layout that puts insights first.

Step 1: Define the Model

Begin by defining the data structures in the StudentPerformanceModels.cs file. Each class represents a specific part of student performance, such as subjects, scores, exam results, participation, or grade distribution.

Here’s how you can do it in code:

public class StudentsPerYear
{
    // Yearly total student count; drives year filter and overall enrollment visuals.
}
 
public class AverageSubjectScore
{
    // Yearly average scores across subjects; powers KPI tiles and score trends.
}
 
public class SubjectScores
{
    // Container for subject score fields (PhysEd, English, Maths, Science).
}
 
public class StudentsByGradeAndGender
{
    // Gender totals for the selected year; used by the gender participation pie.
}
 
public class StudentParticipationRateByBranch
{
    // Yearly participation percentages by subject; feeds subject participation visuals.
}
 
public class BranchRates
{
    // Holder for participation rates per subject (PhysEd/Arts/English/Maths/Science).
}
 
public class ExaminationResultsByBranch
{
    // Yearly exam outcomes by subject (Pass/Fail/NotAttended); used for exam result charts.
}
 
public class ExaminationResult
{
    // Aggregated counts for a subject: Pass, Fail, and Not Attended.
}
 
public class LabelValue
{
    // Generic label–value–percentage tuple for simple charts, legends, and summaries.
}
 
public class SubjectRate
{
    // Pairs a Subject with a participation rate; used in subject participation bars/gauges.
}
 
public class SubjectExamResult
{
    // Per-subject exam outcome counts for a given year; drives stacked bar/pie breakdowns.
}
 
public class SubjectScoreTile
{
    // Simple KPI tile model showing a subject name and its average score.
}
 
public class GradeDistribution
{
    // Grade bucket share (A–F) with color; used in the grade distribution visualization.
}
 
public class Subject
{
    // Subject identity used in filters and charts; optional IsSelected for UI state.
} 

Step 2: Create the ViewModel

Next, we need to define the StudentPerformanceViewModel class. This class connects the data models to the UI, exposes data using ObservableCollection, and keeps the dashboard updated when data changes.

Here’s how that looks in code:

public partial class StudentPerformanceViewModel
{
    public StudentPerformanceViewModel()
    {
        SeedSampleData();            
        InitializeSubjects();        
        InitializeCollections();     
        SelectedSubject = _subjectsByName["Maths"];
        UpdateFilteredData();        
    }
 
    private void SeedSampleData()
    {
        // Seeds year-wise totals, average scores, participation rates, and computes exam results
    }
    
    private void InitializeSubjects()
    {
        // Creates subject cache and list used across UI (includes "All")
    }
 
    private void InitializeCollections()
    {
        // Populates Years list (latest first)
    }
    
    private void UpdateFilteredData()
    {
        // Rebuilds all projections when filters change (year/subject)
    }
 
    private void ComputeExamResultsFromRatesAndScores()
    {
        // Computes examination pass/fail/not-attended per year/subject from participation and average scores
    }
 
    private void BuildProjections()
    {
        // Builds runtime series for the selected year/subject (gender pie, participation by subject, exam results, trends)
    }
 
    private void BuildGradeDistribution(double average)
    {
        // Builds A/B/C/D/F distribution from an average score using a normal-model approximation
    }
}

Step 3: Structure the layout

A Grid layout organizes the dashboard into predictable zones: filters at the top, KPI tiles beneath them, and charts below.

Code snippet to achieve this:

<!--MainGrid-->
<Grid RowDefinitions="1*,1*,3.5*,4.5*">
     <!-- Header and Filters (Grid.Row="0") -->
     <Grid Grid.Row="0" Background="#140733" ColumnDefinitions="Auto,Auto,Auto,Auto,*" Height="70">
         <!-- ... TextBlock for title ... -->
         <!-- ... ComboBoxAdv for Subject ... -->
         <!-- ... ComboBoxAdv for Year ... -->
     </Grid>

     <!-- Score Tiles (Grid.Row="1") -->
     <Grid Grid.Row="1" ColumnDefinitions="*,*,*,*">
            . . . 
     </Grid>

     <!-- Charts (Grid.Row="2") -->
         <Grid Grid.Row="2" ColumnDefinitions="4*,3.5*,2.5*" >
             . . .
             <!-- Chart 1: Exam Results by Subject (Grid.Column="0") -->
             <!-- Chart 2: Semester Grade Trend ( Grid.Column="1") -->
             <!-- Chart 3: Gender Participation ( Grid.Column="2") -->
         </Grid>
         <Grid Grid.Row="3" ColumnDefinitions="4*,4*,2*"> 
             <!-- Chart 1: Students per Year (Grid.Column="0") --> 
             <!-- Chart 2: Participation Rate by Subject (Grid.Column="1") --> 
             <!-- Chart 3: Grades Distribution (Grid.Column="2") --> 
         </Grid>
     </Grid>

UI components and their roles

Top panel: Controls and dynamic filters

The dashboard’s top section includes the main title and interactive filtering elements:

Title

Add a TextBlock to display the title Student Performance and Engagement Hub, as shown in the code below:

<TextBlock
    Margin="20,0,0,0"
    Text="Student Performance and Engagement Hub"
    Foreground="#FAEFF6"
    FontSize="24"/>

Subject Dropdown

You define ComboBoxAdv so users can select a subject. Then bind it to the Subjects ViewModel and update the SelectedSubject property when a choice is made, triggering data recalculations.

Here’s the Subject Dropdown implementation:

<editors:ComboBoxAdv
    Grid.Column="2"
    Style="{StaticResource comboBoxStyle}"
    Width="160"
    DisplayMemberPath="Name"
    ItemsSource="{Binding Subjects}"
    SelectedItem="{Binding SelectedSubject, Mode=TwoWay}"
    VerticalAlignment="Center"/>

Year Dropdown

Add another ComboBoxAdv class to filter data by academic year. You need to bind it to the Years list and the SelectedYear property, updating the dashboard’s data accordingly.

Here’s the year Dropdown implementation:

<editors:ComboBoxAdv
    Grid.Column="3"
    Style="{StaticResource comboBoxStyle}"
    Margin="10,0,0,0"
    Width="120"
    ItemsSource="{Binding Years}"
    SelectedItem="{Binding SelectedYear, Mode=TwoWay}"
    VerticalAlignment="Center"/>

KPI cards: Performance at a glance

You can display each subject’s average score in a compact KPI tile. These cards make performance trends visible before users even look at charts.

Here’s the KPI tile implementation:

<!-- PhysEd Score Tile -->
<!-- Example for one subject, others would follow a similar pattern -->
<Border Grid.Column="0" Style="{StaticResource border}">
    <StackPanel Orientation="Horizontal"
                HorizontalAlignment="Center"
                VerticalAlignment="Center">
        <TextBlock Text="PhysEd : " Style="{StaticResource kpiCard1}"/>
        <TextBlock Text="{Binding PhysEdScore, StringFormat={}{0}%}"
                   Style="{StaticResource kpiCard2}"/>
         ….
    </StackPanel>
</Border>
Subject-wise average core in KPI Cards
Subject-wise average core in KPI Cards

Next, we will display the charts, which are explained in detail in the following section.

Implementing insightful charts

Syncfusion WPF Charts are used throughout the application to deliver clear, interactive, and visually compelling data insights. Below is an overview of the chart types implemented and their purposes.

1. Column Chart: Exam performance breakdown

The Column Chart visually compares examination outcomes, such as Pass, Fail, and Not Attended, across different subjects for a selected academic year. It helps educators quickly identify subjects where students may be excelling, struggling, or disengaging.

We use the SfChart control with multiple ColumnSeries classes, each representing a specific result category, as shown below.

<!-- Column Chart: Examination Results -->
<chart:SfChart Palette="Custom">
    <chart:SfChart.PrimaryAxis>
        <chart:CategoryAxis ... />
    </chart:SfChart.PrimaryAxis>
 
    <chart:SfChart.SecondaryAxis>
        <chart:NumericalAxis ... />
    </chart:SfChart.SecondaryAxis>
 
    <chart:SfChart.Legend>
        <chart:ChartLegend ... />
    </chart:SfChart.Legend>
 
    <chart:SfChart.ColorModel>
        <chart:ChartColorModel>
            <chart:ChartColorModel.CustomBrushes>
                <SolidColorBrush Color="#0BA7AC"/>
                <SolidColorBrush Color="#F77F00"/>
                <SolidColorBrush Color="#EC492D"/>
            </chart:ChartColorModel.CustomBrushes>
        </chart:ChartColorModel>
    </chart:SfChart.ColorModel>
 
    <!-- Series -->
    <chart:ColumnSeries ItemsSource="{Binding FilteredExamResults}" XBindingPath="Subject.Name"
                        YBindingPath="Pass" Label="Pass" ... />
    <chart:ColumnSeries ItemsSource="{Binding FilteredExamResults}" XBindingPath="Subject.Name"
                        YBindingPath="Fail" Label="Fail" ... />
    <chart:ColumnSeries ItemsSource="{Binding FilteredExamResults}" XBindingPath="Subject.Name"
                        YBindingPath="NotAttended" Label="Not Attended" ... />
</chart:SfChart>
Visualizing exam results using a Column Chart
Visualizing exam results using a Column Chart

2. Spline Chart: Semester‑wise grade progression

You can use a Spline Chart to highlight grade progression across semesters, enabling educators to observe long-term performance patterns such as improvement or decline.

A SplineSeries in the SfChart is used to draw a smooth, continuous curve reflecting semester-wise grade changes.

Code example for quick integration:

<!-- Spline Chart: Semester Grade Trends -->
<chart:SfChart>
    <chart:SfChart.PrimaryAxis>
        <chart:CategoryAxis ... />
    </chart:SfChart.PrimaryAxis>
 
    <chart:SfChart.SecondaryAxis>
        <chart:NumericalAxis ... />
    </chart:SfChart.SecondaryAxis>
 
    <chart:SplineSeries ItemsSource="{Binding FilteredSemesterTrend}"
                        XBindingPath="Label" YBindingPath="Value"
                        EnableAnimation="True" Interior="#0BA7AC" ShowTooltip="True">
        <chart:SplineSeries.AdornmentsInfo>
            <chart:ChartAdornmentInfo ShowMarker="True" Symbol="Ellipse" ... />
        </chart:SplineSeries.AdornmentsInfo>
    </chart:SplineSeries>
</chart:SfChart>
Visualizing semester-wise grade using a Spline Chart
Visualizing semester-wise grade using a Spline Chart

3. Doughnut Chart: Gender participation distribution

You can use a Doughnut Chart to display the proportional distribution of student participation by gender (Male, Female, and Others) based on the selected year and subject.

A DoughnutSeries, a variation of the PieSeries with a hollow center, provides a clear and modern representation of gender ratios.

Here’s the Doughnut Chart implementation:

<!-- Doughnut Chart: Gender Participation -->
<chart:SfChart>
    <chart:SfChart.Legend>
        <chart:ChartLegend DockPosition="Bottom" ... />
    </chart:SfChart.Legend>
 
    <chart:DoughnutSeries ItemsSource="{Binding GenderParticipationPie}"
                           XBindingPath="Label" YBindingPath="Value"
                           Palette="Custom" EnableAnimation="True" ... >
        <chart:DoughnutSeries.ColorModel>
            <chart:ChartColorModel>
                <chart:ChartColorModel.CustomBrushes>
                    <SolidColorBrush Color="#0BA7AC"/>
                    <SolidColorBrush Color="#F77F00"/>
                    <SolidColorBrush Color="#EC492D"/>
                </chart:ChartColorModel.CustomBrushes>
            </chart:ChartColorModel>
        </chart:DoughnutSeries.ColorModel>
 
        <chart:DoughnutSeries.AdornmentsInfo>
            <chart:ChartAdornmentInfo ShowLabel="True" SegmentLabelContent="Percentage" ... />
        </chart:DoughnutSeries.AdornmentsInfo>
    </chart:DoughnutSeries>
</chart:SfChart>
Visualizing gender participation distribution using a Doughnut Chart
Visualizing gender participation distribution using a Doughnut Chart

4. Spline Chart: Year‑over‑year student count trend

Again, use another Spline Chart to showcase student enrollment trends across academic years, allowing stakeholders to identify growth or decline patterns.

A smooth curve is achieved using a SplineSeries to visually represent year-over-year changes in student numbers.

<!-- Spline Chart: Students per Year Trend -->
<chart:SfChart>
    <chart:SfChart.PrimaryAxis>
        <chart:CategoryAxis ... />
    </chart:SfChart.PrimaryAxis>
 
    <chart:SfChart.SecondaryAxis>
        <chart:NumericalAxis ... />
    </chart:SfChart.SecondaryAxis>
 
    <chart:SplineSeries ItemsSource="{Binding StudentsPerYear}"
                        XBindingPath="Year" YBindingPath="Students"
                        EnableAnimation="True" Interior="#F73039" ShowTooltip="True">
        <chart:SplineSeries.AdornmentsInfo>
            <chart:ChartAdornmentInfo ShowMarker="True" Symbol="Ellipse" ... />
        </chart:SplineSeries.AdornmentsInfo>
    </chart:SplineSeries>
</chart:SfChart>
Spline Chart showcasing student enrollment trends
Spline Chart showcasing student enrollment trends

5. Column and Doughnut Charts: Subject‑level participation insights

This visualization dynamically adapts depending on the user’s selection:

  • When “All” subjects are selected, a Column Chart is displayed to present a comparative view of participation rates across all subjects.
Comparative view of participation rates across all subjects using a Column Chart
Comparative view of participation rates across all subjects using a Column Chart
  • When a single subject is selected, a Doughnut Chart is displayed to provide a focused breakdown of participation for that specific subject.
Displaying Doughnut Chart to provide breakdown of participation of a subject
Displaying Doughnut Chart to provide breakdown of participation of a subject

This dynamic switching between ColumnSeries and DoughnutSeries is handled by the custom converters OneItemVisibleConverter and MultipleItemsVisibleConverter, defined in Converters/Converter.cs, as shown below.

<!-- Participation by Subject: Multi vs Single -->
<Grid>
    <!-- Multi-subject view: Column Chart -->
    <chart:SfChart Visibility="{Binding FilteredParticipationRates.Count,
                                Converter={StaticResource MultipleItemsVisibleConverter}}">
        <chart:SfChart.PrimaryAxis>
            <chart:CategoryAxis ... />
        </chart:SfChart.PrimaryAxis>
        <chart:SfChart.SecondaryAxis>
            <chart:NumericalAxis Maximum="100" Visibility="Collapsed" ... />
        </chart:SfChart.SecondaryAxis>
 
        <chart:ColumnSeries ItemsSource="{Binding FilteredParticipationRates}"
                            XBindingPath="Subject.Name" YBindingPath="Rate"
                            Palette="Custom" EnableAnimation="True">
            <chart:ColumnSeries.ColorModel>
                <chart:ChartColorModel>
                    <chart:ChartColorModel.CustomBrushes>
                        <SolidColorBrush Color="#087C8A"/>
                        <SolidColorBrush Color="#14A89B"/>
                        <SolidColorBrush Color="#FC9000"/>
                        <SolidColorBrush Color="#F85600"/>
                    </chart:ChartColorModel.CustomBrushes>
                </chart:ChartColorModel>
            </chart:ColumnSeries.ColorModel>
            <chart:ColumnSeries.AdornmentsInfo>
                <chart:ChartAdornmentInfo ShowLabel="True"/>
            </chart:ColumnSeries.AdornmentsInfo>
        </chart:ColumnSeries>
    </chart:SfChart>
 
    <!-- Single-subject view: Doughnut with center label -->
    <chart:SfChart VerticalAlignment="Center" Margin="5"
                   Visibility="{Binding FilteredParticipationRates.Count,
                                Converter={StaticResource OneItemVisibleConverter}}">
        <chart:DoughnutSeries ItemsSource="{Binding FilteredParticipationRates}"
                              XBindingPath="Subject.Name" YBindingPath="Rate"
                              Palette="Custom" EnableAnimation="True"
                              DoughnutCoefficient="0.7" DoughnutSize="1"
                              StartAngle="120" EndAngle="420">
            <chart:DoughnutSeries.CenterView>
                <ContentControl>
                    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
                        <TextBlock Text="{Binding FilteredParticipationRates[0].Subject.Name}"
                                   FontSize="24" HorizontalAlignment="Center" Foreground="White"/>
                        <TextBlock Text="{Binding FilteredParticipationRates[0].Rate}"
                                   FontSize="34" FontWeight="Bold"
                                   HorizontalAlignment="Center" Foreground="White"/>
                    </StackPanel>
                </ContentControl>
            </chart:DoughnutSeries.CenterView>
            <chart:DoughnutSeries.ColorModel>
                <chart:ChartColorModel>
                    <chart:ChartColorModel.CustomBrushes>
                        <SolidColorBrush Color="#f73039"/>
                        <SolidColorBrush Color="#ff6a71"/>
                    </chart:ChartColorModel.CustomBrushes>
                </chart:ChartColorModel>
            </chart:DoughnutSeries.ColorModel>
        </chart:DoughnutSeries>
    </chart:SfChart>
</Grid>

6. Overview of grade performance spread

Finally, implement the grades distribution panel to show how students are spread across each grade category. It highlights toppers, average performers, and those needing extra support.

Try this in your code:

<Grid RowDefinitions="1.5*, 8.5*" ColumnDefinitions="Auto,Auto">
    <!-- Title -->
    <TextBlock Grid.Row="0" Grid.ColumnSpan="2" Text="Grades Distribution" Style="{StaticResource titleStyle}"/>
 
    <!-- Grade List -->
    <ItemsControl Grid.Row="1" Grid.Column="0" ItemsSource="{Binding GradeDistributions}">
 
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="ContentPresenter">
                <Setter Property="Margin" Value="0,6,0,6"/>
            </Style>
        </ItemsControl.ItemContainerStyle>
 
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Vertical" HorizontalAlignment="Left"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
 
            <DataTemplate>
                <Border CornerRadius="14">
                    <Border.Background>
                        <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
                            <GradientStop Color="#FFFFFBEB" Offset="0"/>
                            <GradientStop Color="{Binding Color}" Offset="1"/>
                        </LinearGradientBrush>
                    </Border.Background>
                    <StackPanel Orientation="Horizontal" Margin="10">
                        <TextBlock Text="{Binding Grade}" FontWeight="SemiBold" FontSize="16"/>
                        <TextBlock VerticalAlignment="Center" FontSize="16" Margin="5,0,0,0">
                            <Run Text="{Binding Percentage}"/>
                            <Run Text="%"/>
                        </TextBlock>
                    </StackPanel>
                </Border>
            </DataTemplate>
 
        </ItemsControl.ItemTemplate>
    </ItemsControl>
 
    <!-- Motivation Section -->
    <StackPanel Orientation="Vertical" Grid.Row="1" Grid.Column="1" Margin="30,30,0,0">
        <TextBlock Text="Every grade tells" FontStyle="Italic" Foreground="#FAEFF6" FontWeight="SemiBold" FontSize="17"/>
        <TextBlock Text="a story of effort..!" FontStyle="Italic" Foreground="#FAEFF6" FontWeight="SemiBold" FontSize="17"/>
        <Path  Data="{StaticResource PathData2}" Stroke="#FAEFF6" StrokeThickness="0.6" Margin="0,20,0,0">
            <Path.RenderTransform>
                <ScaleTransform ScaleX="4.5" ScaleY="5"/>
            </Path.RenderTransform>
        </Path>
    </StackPanel>
</Grid>
Grade distribution panel to show spread of children across each category
Grade distribution panel to show spread of children across each category

Here’s a quick demo of the dashboard in action.

Student performance and engagement dashboard using WPF Charts
Student performance and engagement dashboard using WPF Charts

GitHub reference

For more details, refer to the complete student performance dashboard using WPF Charts demo on GitHub.

Frequently Asked Questions

Which charts or libraries are used in this dashboard?

The dashboard uses WPF chart components (such as bar, line, pie/donut charts) to present student performance metrics. These can also be implemented using libraries like .NET MAUI Syncfusion Toolkit Charts or WinUI Syncfusion Charts.

Are the charts interactive (hover, click, filter)?

Yes, the charts can support interactions such as tooltips, click events, and dynamic filtering (e.g., selecting a subject or year), depending on the chosen chart library.

How customizable is the dashboard’s appearance?

The theme, colors, layout, and animations can be fully customized using XAML styling, ControlTemplates, and gradients.

What’s the difference between LineSeries, FastLineSeries, and FastLineBitmapSeries? When should I use each?

LineSeries: Best suited for smaller datasets. It offers complete customization options, including segment dragging and detailed stroke settings.

FastLineSeries: Renders data using a single polyline, making it ideal for large datasets (50,000+ points) with better performance than LineSeries.

FastLineBitmapSeries: Use bitmap rendering to deliver the highest throughput, making it suitable for huge datasets (hundreds of thousands to millions of points), though with fewer styling options

Conclusion

Thank you for reading! In this blog, we explored how to build a Student Performance Dashboard using Syncfusion WPF Charts. We implemented visual components to track subject-wise scores, participation trends, and overall grade distribution. Interactive chart types such as column, line, and pie charts help present academic progress, identify learning patterns, and highlight areas needing improvement.

These insights empower educators and institutions to monitor student performance more effectively, support personalized learning, and make data‑driven decisions that enhance academic outcomes.

We hope this blog post inspires you to leverage the power of WPF and Syncfusion controls to build impactful data visualization applications!

If you’re a Syncfusion user, you can download the setup from the license and downloads page. Otherwise, you can download a free 30-day trial.

You can also contact us through our support forum, support portal, or feedback portal for queries. We are always happy to assist you!

Read the whole story
alvinashcraft
17 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

.NET AI Essentials – The Core Building Blocks Explained

1 Share

Artificial Intelligence (AI) is transforming how we build applications. The .NET team has prioritized keeping pace with the rapid changes in generative AI and continue to provide tools, libraries, and guidance for .NET developers building intelligent apps. With .NET, developers have a powerful ecosystem to integrate AI seamlessly into their apps. This post introduces the building blocks for AI in .NET.

These include:

  • Microsoft.Extensions.AI for unified LLM access
  • Microsoft.Extensions.VectorData for semantic search and persisted embeddings
  • Microsoft Agent Framework for agentic workflows
  • Model Context Protocol (MCP) for interoperability

We’ll explore each library with practical examples and tips for getting started. This is the first of four posts which will cover each of the four areas mentioned.

Introducing Microsoft.Extensions.AI: one API, many providers

The foundational library for interfacing with generative AI in .NET is the Microsoft Extensions for AI, often abbreviated as “MEAI.” If you are familiar with Semantic Kernel, this library replaces the primitives and universal features and APIs that were introduced by the Semantic Kernel team. It also integrates successful patterns and practices like dependency injection, middleware, and builder patterns that developers are familiar with in web technologies like ASP.NET, minimal APIs, and Blazor.

A unified API for multiple providers

Instead of juggling multiple SDKs, you can use a single abstraction for OpenAI, OllamaSharp, Azure OpenAI, and more. Let’s take a look at some simple examples. Here’s the “getting started” steps for OllamaSharp:

var uri = new Uri("http://localhost:11434");
var ollama = new OllamaApiClient(uri)
{
    SelectedModel = "mistral:latest"
};
await foreach (var stream in ollama.GenerateAsync("How are you today?"))
{
    Console.Write(stream.Response);
}

If, on the other hand, you are using OpenAI, it looks like this:

OpenAIResponseClient client = new("o3-mini", Environment.GetEnvironmentVariable("OPENAI_API_KEY"));

OpenAIResponse response = await client.CreateResponseAsync(
            [ResponseItem.CreateUserMessageItem("How are you today?")]
  );
foreach (ResponseItem outputItem in response.OutputItems)
{
    if (outputItem is MessageResponseItem message)
    {

        Console.WriteLine($"{message.Content.FirstOrDefault()?.Text}");
    }
}

To use the universal APIs, you configure your client the same way, but then you have a common API to use to make your requests. The OllamaSharp chat client already supports the universal IChatClient interface. The Open AI client does not, so you can use the handy extension method available in the Open AI adapter.

IChatClient client =
    new OpenAIClient(key).GetChatClient("o3-mini").AsIChatClient();

Now you can use the same API to retrieve a response, regardless of which provider you use.

await foreach (ChatResponseUpdate update in client.GetStreamingResponseAsync("How are you today?"))
{
    Console.Write(update);
}

Beyond convenience: what happens behind the scenes

In addition to handling delegation to and from the provider, the universal extensions also manage retries, token limits, integrate with dependency injection and support middleware. I’ll cover middleware later in this post. Let’s look at an example of how the extensions work for you to simplify the code you have to write.

Structured output the super easy way

Structured output allows you to specify a schema for returned output. This not only allows you to respond to output without having to parse the response, but also provides more context about the expected output to the model. Here is an example using the Open AI SDK:

class Family
{
    public List<Person> Parents { get; set; }
    public List<Person>? Children { get; set; }

    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

ChatCompletionOptions options = new()
{
    ResponseFormat = StructuredOutputsExtensions.CreateJsonSchemaFormat<Family>("family", jsonSchemaIsStrict: true),
    MaxOutputTokenCount = 4096,
    Temperature = 0.1f,
    TopP = 0.1f
};

List<ChatMessage> messages =
[
    new SystemChatMessage("You are an AI assistant that creates families."),
    new UserChatMessage("Create a family with 2 parents and 2 children.")
];

ParsedChatCompletion<Family?> completion = chatClient.CompleteChat(messages, options);
Family? family = completion.Parsed;

Let’s do the same thing, only this time using the extensions for AI.

class Family
{
    public List<Person> Parents { get; set; }
    public List<Person>? Children { get; set; }

    class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
}

var family = await client.GetResponseAsync<Family>(
    [
        new ChatMessage(
            ChatRole.System,
            "You are an AI assistant that creates families."),
        new ChatMessage(
            ChatRole.User,
            "Create a family with 2 parents and 2 children."
        )]);

The typed extension method uses the adapters to provide the appropriate schema and even parse and deserialize the responses for you.

Standardized requests and responses

Have you ever wanted to change the temperature of a model? Some of you are asking, “What’s temperature?” In the “real world” temperature is a measure of the energy of a system. If particles are standing still, or frozen in place, it’s cold. Heat means there is a lot of energy and a lot of movement at least at the microscopic level. Temperature influences entropy, which is a measure of randomness. The same concept applies to models.

If you remember from the introductory post, models are basically huge probability engines that roll the dice. Sort of. If you set the temperature low, the model will produce more predictable (and usually factual) responses, while a higher temperature introduces more randomness into the response. You can think of it as allowing lower probability responses to have a bigger voice in the equation, which may lead to ungrounded responses (when the model provides information that is inaccurate or out of context) but can also lead to more “creative” appearing responses as well.

More deterministic tasks like classification and summarization probably benefit from a lower temperature, while brainstorming ideas for a marketing campaign might benefit from a higher temperature. The point is, models allow you to tweak everything from temperature to a maximum token count and it’s all standardized as part of the ChatOptions class.

On the flipside, when you receive a response, the response contains a UsageDetails instance. Use this to keep track of your token counts.

The middle is where?

.NET web developers are already familiar with the power of middleware. Think of it as a plugin model for your workflow. In this case, the workflow or pipeline is the interaction with the model. Middleware allows you to intercept the pipeline and do things like:

  • Stop malicious content from making it to the model
  • Block or throttle requests
  • Provide services like telemetry and tracing

MEAI provides middleware for telemetry and tracing out of the box. You can use the familiar builder pattern to apply middleware to an existing chat client. Here’s an example method that adds middleware to any existing client – regardless of provider – that will handle both logging and generation of Open Telemetry (OTEL) events.

public IChatClient BuildEnhancedChatClient(
            IChatClient innerClient,
            ILoggerFactory? loggerFactory = null)
        {
            var builder = new ChatClientBuilder(innerClient);

            if (loggerFactory is not null)
            {
                builder.UseLogging(loggerFactory);
            }

            var sensitiveData = false; // true for debugging

            builder.UseOpenTelemetry(
                configure: options =>
                    options.EnableSensitiveData = sensitiveData);
            return builder.Build();
        }

The OTEL events can be sent to a cloud service like Application Insights or, if you are using Aspire, your Aspire dashboard. Aspire has been updated to provide additional context for intelligent apps. Look for the “sparkles” in the dashboard to see traces related to interactions with models.

Example Aspire dashboard with sparkles indicating LLM interactions

DataContent for Multi-Modal Conversations

Models today do more than simply pass text back and forth. More and more multi-modal models are being released that can accept data in a variety of formats, including images and sounds, and return similar assets. Although most examples of MEAI focus on the text-based interactions, which are represented as TextContent instances, there are several built in content types available, all based on AIContent:

  • ErrorContent for detailed error information with error codes
  • UserInputRequestContent to request user input (includes FunctionApprovalRequestContent and FunctionApprovalResponseContent)
  • FunctionCallContent to represent a tool request
  • HostedFileContent to reference data hosted by an AI-specific service
  • UriContent for a web reference

This is not a comprehensive list, but you get the idea. The one you will likely use the most, however, is the DataContent that can represent pretty much any media type. It is simply a byte array with a media type.

For example, let’s assume I want to pass my photograph to a model with instructions to describe it and provide a list of tags. Assuming my photo is stored as c:\photo.jpg I can do this:

var instructions = "You are a photo analyst able to extract the utmost detail from a photograph and provide a description so thorough and accurate that another LLM could generate almost the same image just from your description.";

var prompt = new TextContent("What's this photo all about? Please provide a detailed description along with tags.");

var image = new DataContent(File.ReadAllBytes(@"c:\photo.jpg"), "image/jpeg");

var messages = new List<ChatMessage>
            {
                new(ChatRole.System, instructions),
                new(ChatRole.User, [prompt, image])
            };

record ImageAnalysis(string Description, string[] tags);

var analysis = await chatClient.GetResponseAsync&lt;ImageAnalysis&gt;(messages);

Other highlights

Although these are out of scope to cover in this post, there are many other services the base extensions provide. Examples include:

  • Cancellation tokens for responsive apps
  • Built-in error handling and resilience
  • Primitives to handle vectors and embeddings
  • Image generation

Summary

In this post, we explored the foundation building block for intelligent apps in .NET: Microsoft Extensions for AI. In the next post, I’ll walk you through the vector-related extensions and explain why they are not part of the core model, then we’ll follow up with agent framework and MCP.

Until then, I have a few options for you to learn more and get started building your intelligent apps.

Happy coding!

The post .NET AI Essentials – The Core Building Blocks Explained appeared first on .NET Blog.

Read the whole story
alvinashcraft
18 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Talking Context on the iBuildWithAI Podcast

1 Share


Ready to learn how to think about agents?

👉 Join me for Mastering Agent Skills — a hands-on workshop to get you up and running building your own skills library.


  • Context is what makes knowledge work valuable: Knowledge workers carry rich context in their heads—history, decisions, preferences, and understanding of why things are done a certain way. AI has general knowledge but lacks this specific context, so making it explicit is essential.

  • Context engineering means making the implicit explicit: Rather than keeping knowledge in people’s heads or scattered discussions, context engineering is the practice of curating, maintaining, and managing information so AI can access and use it effectively.

  • Tools give AI memory and the ability to act: When models can read and write files or interact with external systems, they gain the ability to maintain their own context over time and participate in their own learning.

  • Agent Skills are the emerging standard for dynamic context: Instead of loading everything at once, skills let you label and describe bundles of context that the model can fetch when needed. This keeps things manageable and relevant.

  • Non-engineers often benefit most from AI tools: Power users and knowledge workers who lack coding experience can achieve remarkable things with AI. The main gap is knowing what’s possible, not writing code.

  • The challenge is conceptual, not technical: Learning to think like an agent—understanding what information would help a model that has no prior knowledge of your situation—takes practice, but anyone can develop this skill.

  • Communicate intent, not exact steps: Just as good managers tell people what needs to be done rather than dictating every action, effective context engineering shares goals and constraints while leaving room for the model to be creative.

  • Use context for things you do repeatedly: If you only need something once, a prompt is fine. But for recurring tasks or knowledge you’ll reference often, that belongs in persistent context.

  • Organisations should have a Context Owner: Someone accountable for preparing and maintaining context helps teams adopt AI faster and ensures the systems everyone uses have proper, well-curated information.

  • The future is more dynamic context: As agents become more capable, context engineering is shifting from carefully constructed retrieval systems toward giving agents pointers and letting them fetch what they need on demand.

Marcelo: Welcome to another episode of the I Build With AI podcast, where I have conversations with humans who build apps with AI. I’m your host, Marcelo Lewin.

In today’s episode, I’m chatting with Eleanor Berger, AI leadership expert and founder at Agentic Ventures, all about Context Engineering for knowledge workers. But before we get started, you can help us grow the podcast by reviewing it, following it, and sharing it.

Marcelo: Eleanor, welcome to the podcast. Glad to have you here.

Eleanor: Hey Marcelo, thanks for having me.

Marcelo: It’s great to have you here. You wrote a blog article, and that’s where I actually saw your article on context engineering. It was a while ago, it was on LinkedIn, and I reached out to you and I’m like, “You’d be a perfect guest for that.” So, thank you for being here and for agreeing to be on the podcast. I’m very happy about that. Why don’t we start with your background? Tell us a little bit about your background, how you got into AI, how you got into vibe coding.

Eleanor: Sure. So I’m a software engineer by trade. I’ve done this for many, many years, and eventually moved more to also leading engineering organisations. I’ve worked in quite a few startups and later on at Google and at Microsoft. I got interested in AI... well, I guess I got interested in AI as a child reading science fiction books, but it was unattainable. But then, about ten years ago, a bit more, when deep learning started showing so much promise, I realised I have to get into it. So I learned deep learning and machine learning and got more and more into AI, and it became a bigger part of my career.

And it kind of took a boost all of a sudden, you know, when generative AI became so powerful, because for the first time, there was a lot of work with customers, with internal products and so on. And so it kind of became the main thing I do. Yeah, and now it’s I guess kind of very central to everyone. So I was a bit lucky, in the right place at the right time, sort of coming purely from a fascination with AI.

Marcelo: Most definitely. I mean, AI is definitely taking over. Yesterday I had a great conversation—well, this would be two weeks ago when I publish this podcast—but with Google VP of AI solutions there, and they’re embedding AI pretty much everywhere in every app, right? We were talking about what a great time it is. Scary for some people, totally understand that, but also what an incredible time to be alive and going through this revolution and be able to not only participate in it, but also to influence it.

Eleanor: Yeah, it’s absolutely crazy right now. I mean, in the most positive way I can say that.

Marcelo: Right. Yeah. Now, how did you get into vibe coding?

Eleanor: Obviously being into AI, I was interested in how it can be used in software development very early on. I did a lot of different experiments. The models kept improving. So initially, there was a lot of orchestration trying to get something to work. But then last year really, we started to see really good models that can do proper Agentic software engineering or coding or vibe coding, whatever you call it.

That became... at this point, I kind of moved... I used to work at Microsoft, but I left there and started a consulting practice. And initially I was helping people with AI engineering, integrating AI into their systems. But increasingly people just wanted to hear about AI in software development. And so it was really in response to demand. And of course, in my own work, it became a more and more important thing.

And so eventually, after giving the same advice to people again and again, I, together with a friend, started a course where we teach about AI-assisted software development. We’re now having the second cohort of that course, and it’s going great. Obviously, that’s very central now. Everyone’s interested in that. It’s nice to be able to help people do this transition, move into this world of Agentic coding.

Marcelo: Definitely. So in that course that you’re teaching, are you finding that it’s mostly software developers attending it? Because my audience, right, are technical but non-software developer people. So knowledge workers, domain experts, business professionals, that are starting to vibe code and create and build products or tools or workflows using agents. What kind of audience are you finding in your courses attending that?

Eleanor: You know, that’s really interesting because when we started, my assumption would be it would be best to teach software developers. They’ll make the most use, they’ll have the most benefit out of adopting these tools. And definitely that has been the case with some people, where they’re kind of integrating AI engineering into their existing practice. But what I realised is the most benefit actually accrues to people who are not coming from software engineering. People who are just kind of, let’s call them power users. You know, they just have this passion and a little bit of chutzpah for building something, and they realise the tools are now available. They don’t have to know how to code necessarily. They don’t need to have a lot of engineering experience.

And it’s been incredibly gratifying to support these people on their journey and see amazing things that they are able to do. And in fact, it influenced a lot my own practice and my own career because I decided to shift a little bit and focus more on supporting power users, realising how powerful it is. So this is something that surprised me and is absolutely delightful.

Marcelo: Well, it’s funny you say that because my original site was icode with ai and I rebranded it to ibuildwith.ai because my focus became these power users, domain experts, knowledge workers that are not developers. Because I personally feel that is the future for vibe coding, right? It’s going to be a huge audience where they’re going to be building a lot. And we want to make sure that what they build is done properly. Even though they won’t hand code, they still need to be able to build something that can go to production.

Eleanor: Yeah. And I think in many cases, the biggest difference between people who come from software development and ones who are picking up these tools maybe with less experience, is knowing what’s possible. Because you don’t really need to write the code. And increasingly the models are so good, they’ll do a very decent job. So initially, maybe like a year ago, I would tell people, “Hey, don’t release anything that you wrote like this with AI because maybe it’s not really in the quality required.” I don’t think that’s a problem anymore. Actually, that’s okay now. But often when you work with people who are these kind of power users or, as you call them, knowledge workers, they know what they’d like to achieve, but they don’t know what tools are available or what techniques are available. And this is where it’s possible to help them a lot.

Marcelo: Yeah, completely. Well, and now people are probably wondering, well, what does all this have to do with context engineering, which is what this podcast is about, right? But it’s actually everything to do with context engineering. So why don’t we start with just defining context engineering? Define what it is. How is it different also from prompt engineering? Because everybody, for the beginning of 2025, we’re talking about prompt engineering. Then in the middle of 2025 to the end, it shifted to context engineering. So let’s define it and talk about the differences between that and prompt engineering.

Eleanor: Yeah, I mean, I think the “engineering” word there is a little bit maybe an exaggeration in the context of people who are not actually doing engineering work, and it can be scary to people. So I’d encourage people to ignore it and just think about Context and the centrality of context to everything we do.

You’re talking about knowledge workers. What does it even mean to be a knowledge worker, right? It means being someone who has a lot of context. That’s what everyone does. Go to any office building or any Zoom meeting anywhere, and you’ll find people with very rich context. They understand the history of what they’re doing, why things are done the way they’re done, to what end, what’s important about it. All the little details, decisions that have been made, little opinions and tastes and choices. This is what they do.

Now, to work with AI, to get the benefits from AI, AI needs to have this context. It is sort of implicit, it’s in the air when we work with people. But when you first encounter an AI model, an LLM, it doesn’t have all this context. It has general knowledge, right? It knows everything on the internet. But it doesn’t have the context that you have. And so context engineering or context ownership or context setting, whatever you want to call it, is the practice of taking this context that’s until now mostly in people’s head—even teams and companies that are relatively good at documenting usually they don’t have enough rich context in a way that AI can read—and making this context explicit. Curating it, maintaining it, managing it.

And when you have really good context, AI models can do amazing things. They’re so powerful. But without context, they’re kind of... they’re bland. They have only general knowledge. They don’t understand what’s going on. And so this is central. And I think at every level, from engineering large systems to, like you say, a knowledge worker who is just sort of trying to integrate AI into their own practice or into their team’s work, being able to set context, to maintain it, to evolve it is central. It’s so important.

Marcelo: And it’s interesting you said that, to maintain it and evolve it. Because prior to AI, documentation—context—was second thought. It was post, right? The designing and the building of the application. Now it has become mainly... basically the source code. And the output seems to be sort of the byproduct, the binary building of that source code, right?

Eleanor: Yeah, because I think for most people—there are some exceptions, they’re quite rare—but for most teams, for most people, the context wasn’t in documents. It was in their heads. It was in memories. It was in some discussion they had in the cafeteria the other day. It was in some organisational cultural history. It wasn’t actually written down. The documentation, like you said, was an afterthought. And so it is very different. AI doesn’t have these memories. It’s kind of like a fixed blob. Anything you don’t provide in the form of text doesn’t exist.

Marcelo: Now, context also includes tools. So maybe you can explain a little bit about what does that mean when it comes to context, these tools?

Eleanor: Yeah. Everything that the model has access to that isn’t already in the model, which is fixed. So models have become very good at calling tools, which really means having some interaction with the outside world, except for in chat. Anything from reading a file on the file system or in your Google Drive or SharePoint or whatever it may be, to writing a file. Think about it: a model that is hooked up to a tool that allows it to read files and write files, all of a sudden has memory. That’s very powerful because it can itself maintain this context over time.

It can take actions. Maybe it’s a tool that can virtually click a website and do something that previously a person would have to do and maybe feedback to the model. And so the ability to interact with the environment gives the model the ability to participate in its own context engineering, if you will.

Marcelo: So these tools can then, like we do as humans, right? We don’t need all the context for every project all the time. We need certain things throughout the phases of the project. That’s where sort of like dynamic context comes in, right? Depending on which phase of the project you’re at, you may need a different piece of information. Maybe we can talk a little bit about dynamic context engineering or... we can just say dynamic context building. Or I think it’s also a term used is progressive, right?

Eleanor: Progressive, yeah. So probably the best thing to look at right now is Agent Skills, because they’re the emerging standards. They’re not the only option, but they’ve become the emerging standard. Again, if we want to work a little bit by analogy, you don’t have everything... no one has everything in their head all the time. We have maybe books on a shelf or folders with some documentation we wrote. And we know that it’s there. We know that we might need it one day and where to get it from.

We can have the same with AI with skills, which is just this very simple format. It’s done by giving like a label and a description for every skill, which is a bundle of context, and telling the model: Here, you have all these little different bits of knowledge, of skills. Go and get them if and when you need them. Not all the time, because you can’t have it all at once loaded.

And there are of course other ways. So the model could make a call to a website and do a Google search and read back the information when it needs. These are all options for getting context dynamically. Directing that in itself requires some work. Context engineering or context ownership or whatever you want to call it. Because we don’t want a model that sort of has infinite access to everything, including some things that could be dangerous, right? The internet can be a dangerous place sometimes. Or some things that can be confusing. There’s contradictions, things that are not relevant. So you want to direct also the process of getting context dynamically. But when you do that, you all of a sudden have this much richer palette you can work with.

Marcelo: Definitely. Where do you think non-engineers, non-developers—like knowledge workers, like I’m putting them all into one bucket—but non-engineers would struggle with building proper context? Because the key is building the proper context, the proper amount, at the proper time.

Eleanor: I think, and I also know from my experience now working with many people and teams, the struggle is more conceptual than technical. Because technically it’s all very easy because all this context is always text, right? And we all understand text, it’s very natural for us.

Where people struggle is thinking... let’s say, thinking like an agent. Trying to look at the world through the eyes of an agent that has general knowledge but no specific knowledge, no context. And understanding what would help this model. And it requires sometimes a bit of a mindset shift. But primarily people get it through practice. So when I work with people and they start, for example, building Agent Skills, initially it’s very confusing: How much should I put in it? How little? Should it be more specific? Less specific? Do I need to categorise it in some way? Is it okay to just dump some knowledge in there?

And by the time they iterate on the same thing, you know, seven times, ten times, twenty times, you get a feel for it. And I think that’s the best way to learn it. And anyone can learn it. You don’t need to be an engineer. You don’t need to be, I don’t know, a PhD in AI. All you need is practice because it feels a little bit different than interacting with people, teaching a colleague or something like this. But it’s not radically different. It’s a little bit different. It’s nuanced.

Marcelo: To me, it seems like we do context engineering every day at work in our projects, right? When we’re dealing with colleagues, we don’t give a person everything that we know. We give them the right information at the right time for the particular task we’re trying to accomplish together. And then move on to a different colleague and do something a bit different. That in itself is context engineering. Would you agree?

Eleanor: Yeah, absolutely. And we will have what is called, I guess in psychology, a Theory of Mind. We’ll make some assumptions, usually correct because we understand people well, what’s going to be beneficial for them? What it’s like to be that other person? And getting just enough information, not too little, but also not too much. And we can learn this just as we did working with colleagues in the office. We can learn to do the same with AI. It just takes a bit of practice.

Marcelo: So what type of work do you feel is most beneficial for context engineering versus just a prompt? And maybe you can give an example. I don’t want to put you on the spot, but, you know, if you can give an example of, “Oh, for this, a prompt is good enough, but for this portion, it would be more context engineering.” Provide an example of what you mean by that.

Eleanor: I think the best distinction is something you do once or something you do multiple times. Or something you need to know only once versus something you’ll need to know multiple times. So if you only need to do something once or you only need to share some knowledge with the model once, yeah, you just type it or or paste it or whatever, and it’s a prompt.

But if you want to teach the model to do something... if you want, I don’t know, there’s some workflow you do. Every day I have to write a summary email for my department. And it’s always the same. I don’t want to have to prompt it every day. It doesn’t make sense. I’m not really solving anything for myself, and I’m not making the AI work more efficiently if I have to prompt it every day with the exact structure and the information that needs to be included. That should be in the context.

Marcelo: So it’s almost like building templates of context that can be reused. I mean, that’s where skills come in, right?

Eleanor: I think templates is one aspect of it. So there are different kinds of skills, different kinds of context. The skill is just as the mechanism for including context. Some of them look like templates. Like, “Here’s how you write the kind of email that we send every Friday to management.” And it’s like a template. It will always look the same, but you’ll put different information there. There are other things that look more like documentation, like a library. Like, “Here’s everything you need to know about Project X.”

Marcelo: Like a style guide or...

Eleanor: Yeah, exactly. A style guide or a history or something like this. And there are others that look more like workflows. Whenever we need to do something, you do one, and then you do two, and then if X you do three, and if Y you do four. So that’s more like workflow. All of these things look the same. They’re just a bunch of text. But your expectations for how the AI will use it are a bit different.

Marcelo: How do you strike the balance of giving structure to your context, but also allowing the LLM do what it does best, which is being non-deterministic and creative, right? So that way you’re not putting it in a box, and then you will never get anything new. So, how do you strike that balance when you’re building context of letting it do its non-deterministic thing within a structured framework?

Eleanor: Yeah, that’s a brilliant question, right? Because that’s exactly where you need to get a feel for it. Where there isn’t like a clear rule you could follow every time. Again, by analogy, think about your colleague. You’re teaching your colleague to do something. If you’d give them like a flowchart of exactly what to do... First of all, they’re not going to like you very much because no one likes being told what to do in this way. But also, they’ll never have any original thought. Right? I worked for years as a manager. The first thing I learned is don’t tell people what to do because they’ll stop thinking, they’ll stop being creative. Tell them what needs to be done.

So that’s one way of thinking about it. Communicate to the AI the intent. What do you want? Like, what does it mean for this to be good? What are you trying to achieve? What are your goals? Then there are some things, you know... it always needs to... the title always needs to be in block capital in colour blue. That’s not negotiable. But everything else maybe it can be.

When you are able to communicate your intent, communicate your constraints, but not dictate everything, you’re benefiting from the intelligence and the creativity of the model. You’re letting it think for itself, be inventive sometimes. It’s not always obvious the first time, so you iterate. You try different options, you test them again and again. A lot of what you do with AI is kind of empirical. You try something and you see what the results are like, and then maybe you change something and you try again until you get it right. But after a while you also develop intuitions for how to do this well.

Marcelo: It’s interesting you said that because in my Cody product builder that guides users from zero to one building products, originally I had a very strict “Make sure you ask these ten questions.” And then I learned about outcome-based engineering, where you don’t tell a task a particular [set of] questions. What I ended up doing is setting up ten categories that the AI must have enough information about before it can move forward to the next step in the workflow. So we went from “Ask these ten questions” to “Make sure you satisfy enough information you need based on these ten categories.” And now it asks whatever questions it needs to be able to satisfy that outcome.

Eleanor: That’s a great example, right? So if you ask me something I already told you, that’s annoying and it wastes time. But if I said in the context, “These are the things that are needed for completing this task,” then you can decide, okay, these things I could already infer—they’re obvious, or you told me them—and these other things I’m going to have to ask you. So that’s a great example.

Marcelo: Yeah. And it’s almost... like you mentioned this before, where this is how you should really manage people: Don’t tell them exactly how to do it. Tell them what you want the outcome [to be] and then let them use their creativity to figure out the best way to do it. Of course, you can give guidance, right, as you go along if asked. But at the end of the day, within professionals, we don’t tell each other “Exactly do it these ten ways.” We say, “Hey, this is what we need. This is why we need it. Now you go for it and figure out the rest.”

Eleanor: Yes. And that’s exactly how we should work with AI agents. It’s more robust. Or maybe “antifragile” if you’d like to use this nice term. When we define everything very rigidly, it’s brittle. Something is going to break. It will not work exactly like we expected, and then what? But when we communicate intent, what does a good outcome look like? What are the constraints? Then it’s adaptable. It can figure out what to do based on the context.

Marcelo: And it seems the better a person gets at context engineering and speaking to AI, it actually can translate into being a better communicator, at least in a professional manner, at work.

Eleanor: Perhaps. I’d be a bit careful. We shouldn’t talk to other people like we talk to machines. But it does help you think... I mean, I guess it makes you a good manager. There are things that people like me who spent many years trying to become good managers can now probably learn in a few weeks because you very quickly iterate in a way you can’t with people.

Marcelo: Right. That totally makes sense. And I agree with you, we shouldn’t talk to humans like we talk to machines. But there is a little bit of, “Oh, I see, if I give this extra more information, it understands me better. So maybe I should do that also in real life.”

Eleanor: Yeah, definitely.

Marcelo: So what roles do you think leadership can play in a company to support context engineering efforts?

Eleanor: Have an explicit role of a Context Owner. Sometimes it can be a small group depending on the size of the organisation. It can be either one person or a group that’s responsible for it. And the reason for that, first of all, adoption of AI is not equal. Some people need a bit longer to get used to it. Some people are very eager, they’re enthusiastic about it and they want to move forward. And so it’s a perfect role for someone who’s very enthusiastic about AI and they’re asking, “How can I help our team adopt AI faster and better?”

And ownership means accountability and responsibility. So someone to think, “Okay, it’s my role... Like, everyone here will be using the AI systems, and if I didn’t make sure that I prepared the context... Doesn’t mean that I have to do it all myself, maybe I have to review contributions or make decisions... but it’s on me to make sure that the AI system that everyone is using has proper context.” This works really well. I think in my experience, this is usually what unlocks, at least initially, the first steps. You’re not requiring everyone to be equally good at this. You’re saying it is an important function for us, it’s something that will matter for all of us, but here, Marcelo is going to take it on himself and we’re very lucky to to be able to to benefit from his work.

Marcelo: Now, there’s a couple of ways of working with AI, right? One of it is where kind of like Claude Code or in Copilot or whatever you choose, where it’s very interactive. And what I mean by that is you tell it through context engineering and prompt engineering, you tell it to do certain things, it comes back and you iterate and it involves the human. It’s the human in the loop constant iteration, right? But the other way, which is starting to take off quite a bit, right, is sending autonomous agents to go make a... do a task, perform a task, and then come back with the finished task. No interaction with the human. I would assume that context engineering, context building, are very different for each of those approaches.

Eleanor: I don’t think context engineering should be different, but I think that the ability to specify a task, to communicate intent, is really much more important when you are delegating to an autonomous agent. And in a way, this is the key, right? So I think there is a lot of focus right now on this interactive work and what people call maybe “vibe coding.” It’s very exciting. Like emotionally, it’s kind of amusing to do this. I think it really... right now there’s a lot of interest in it from people who are experiencing it for the first time.

But the truth is, to really benefit from AI, it doesn’t help necessarily to have a human in the loop keeping prompting and trying again and again and again. This is actually... introduces a lot of randomness. It doesn’t improve the skills of the person doing the work because they are not getting better at specifying things clearly. And it’s not improving the quality of the work done by the AI because that’s exactly what isn’t helpful to AI.

So I think it’s a good introduction for many people to experiment a little bit with a chatbot or with Claude Code or something like this in a very interactive, turn-by-turn mode. And after a while, when people want to really make the most of working with AI, really unlock productivity improvements, really get better quality software and get better collaboration when working in a team or in a larger distributed project, this happens much better by figuring out how to delegate complete tasks with, again, strong intent communication and clear context. That works a lot better.

Marcelo: Yeah, definitely. A couple more questions, we’re almost at the end of the podcast here. I wanted to get a couple of terms out of the way because I think these are terms that people will hear about when they’re building context and they’re new to it, and they may not be aware of. But maybe you can address Context Pollution and Context Window itself.

Eleanor: Context Window is the amount of text the model can look at at any given time. And it is finite. It is advertised depending on which model somewhere around 200,000 tokens, which are these like sub-word units, to even a million. Effectively it’s less. So I would say something like hundreds of thousands of these tokens. You can imagine it’s something like maybe 300,000, 400,000 words in English. Now that’s a lot. It’s a huge amount of text, but it’s also limited. It’s finite.

If you really want to have a lot of context for your project or maybe for an entire organisation, it would have to be dynamic. You can’t stuff it all at once into the context window. And so that’s worth being aware of. And this is why a lot of people are thinking now about techniques and approaches for loading context dynamically and progressively. Agent Skills being the now the standard really solve this problem very elegantly by not loading it all at once, but giving it a title and a description and allowing the model to load it dynamically when needed. In other cases, is by running searches maybe on a database or on an intranet or anything like this, and getting it into the model on demand when it needs it.

Marcelo: And if you do load it all at once, that’s where you can get into Context Pollution, right? Maybe explain a little bit about that.

Eleanor: Yeah. It’s not helpful. It wouldn’t be helpful to you if I gave you information that is wrong and contradictory. Because then you’d have to think like, “Okay, so here it says that it’s black and there it says that it’s white. Which is which? I’m going to have to figure it out.” So the more precise you can be in the context, the better. And it’s not always so trivial because in our world there is a lot of ambiguity and contradiction. So if we can spare the model this ambiguity and contradiction and not expose it to information that is confusing, we are going to get better results.

Marcelo: Right. And it’s also kind of like when you give information that is... that has nothing to do with what you’re trying to do, right? So if I’m telling you to paint the house and I give you a background of when and where I was born, I mean, that’s interesting information, but it has nothing to do with painting the house.

Eleanor: Sure. But I don’t know if it’s going to be detrimental to the performance of the model except for the fact that this is additional context. And the performance of models, it does degrade the more context you add.

Marcelo: Right. So where do you see context engineering evolving? How do you see it evolving in the future?

Eleanor: I think it’s becoming more and more dynamic, right? So with agents now, we’re allowing... we’re giving the agents more control to go and explore. If in the past, pretty recent past, maybe a year ago, context engineering was a lot about building a retrieval system where we very precisely decide what to feed into the model. That still exists, but in more dynamic systems, if you work for example with an agent like Claude Code or Copilot or any of these things, you don’t do too much of that. You’ll just give pointers to the agent and tell it, “There is information here, go get it if and when you need it.” So that’s definitely a trend we’re seeing. And it depends a bit on scale. When you work at large scale, like real engineering projects where you need to, I don’t know, serve millions of users efficiently, you would invest a lot more rather than do it dynamically because you want to cut latency, you want to cut costs, you want to have some guarantees about the quality of what you’re producing. Whereas in interactive work between a person and an agent, you have a lot more flexibility, right? If the agent got something wrong, I can nudge it. I can tell it, “Oh well, that’s not quite it. Try again.”

Marcelo: Well, Eleanor, thank you so much for being on the podcast. This was a great conversation. I really appreciate it.

Eleanor: Thank you for inviting me. It was great.

Marcelo: And if people want to get a hold of you, we have links on the podcast page, but do you want to give any other kind of link?

Eleanor: Sure. I think the best thing is go to agentic-ventures.com. That’s my platform for helping people work with Agentic systems. A lot of stuff I’m really excited about, including opportunities both in workshops and in offline content to learn about Agent Skills and the Agentic platform. So come and get it.

Marcelo: Definitely. Well, thank you so much, Eleanor. I really appreciate it.

Eleanor: Thank you, Marcelo.




Read the whole story
alvinashcraft
18 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Building AI Agents with TypeScript AI SDK

1 Share

Learn how to build autonomous AI agents using Vercel AI SDK v5 and TypeScript. Create a production-ready search and summarization agent with Experimental_Agent class, complete with practical examples.

Imagine asking your AI assistant: “What’s the latest news on quantum computing breakthroughs, and can you summarize the most significant paper you find?” With basic tool calling, the AI might fetch a URL, read it once and call it a day. But what if the first result isn’t quite right? What if it needs to search again, check multiple sources or dig deeper into specific claims?

This is where AI agents shine. Unlike simple tool calls that execute once and stop, agents can think, act, evaluate and iterate—all on their own. They’re not just calling functions, they’re pursuing goals.

In our previous guide on tool calling, we built a chatbot that could check the time and fetch URLs. That foundation was crucial, but we’re about to level up significantly.

In this tutorial, we’ll build a production-ready AI Search and Article Summarizer. Think Perplexity, but in your terminal. It autonomously searches the web, analyzes multiple sources and delivers comprehensive summaries.

By the end, you’ll understand not just how to build agents, but when they’re the right tool for the job and when simpler patterns make more sense.

What Is an AI Agent?

An AI agent is a program that leverages an LLM and can use tools repeatedly (in a loop) to accomplish complex tasks autonomously. The key word here is “autonomously.” While basic tool calling requires you to explicitly orchestrate each step (call tool, get result, call model again), an agent handles this orchestration itself.

But you’ll likely see the term “agent” used in various ways. I like how Simon Willison has settled on this simple description: An LLM agent runs tools in a loop to achieve a goal.

Basic tool calling is like giving someone a phone and asking them to look up a restaurant’s number—they make one call and report back. An agent is like asking someone to book you a table at a nice Italian restaurant—they’ll search for options, check reviews, call to confirm availability, compare prices and keep iterating until they’ve accomplished the goal.

The agent’s loop continues until it either completes the task successfully, exhausts its allowed steps or determines it cannot proceed further. This persistence and autonomy is what makes agents powerful for complex, multi-step workflows.

Agent vs. Tool Calling: The Evolution

If you’ve worked through my previous article, you’ve already used tools with generateText(). You defined tools, the model decided when to call them, and the AI SDK executed them. That’s tool calling in its simplest form.

But here’s what you had to manage manually:

  • The conversation loop: After each tool execution, you needed to append results to the messages array and call generateText() again.
  • Stopping conditions: You had to decide when the back-and-forth should end.
  • Context management: You controlled what history the model saw at each step.
  • Error handling: Tool failures required your explicit intervention.

Agents encapsulate all of this. The Experimental_Agent class handles the entire loop, manages conversation history automatically, applies stopping conditions and gives you clean hooks for observing what’s happening. You focus on defining the agent’s capabilities and goals; the SDK handles the orchestration.

When Should You Build an Agent?

Agents aren’t always the answer. Sometimes you want explicit control over each step. But agents excel when you have:

  • Research and analysis tasks where the path isn’t predetermined. Your user asks a question, and the agent needs to search, read multiple sources, cross-reference information and synthesize findings. You can’t hard-code this workflow because you don’t know how many searches or which sources will be needed.

  • Multi-step problem solving where decisions depend on previous results. Think of debugging assistance: the agent runs code, analyzes the error, searches for solutions, tries fixes and keeps iterating until the issue is resolved. Each step informs the next, and you can’t know up front how many iterations will be required. You’re probably familiar with this if you’ve used tools like GitHub Copilot or Claude Code.

  • Dynamic task execution where the agent needs to adapt based on what it discovers. Booking a holiday might involve checking flight availability, comparing hotels, reading reviews and checking visa requirements. The agent needs to pivot based on what’s available and what constraints it encounters.

  • Exploratory workflows where you want the AI to “think” through a problem creatively. Writing assistance, brainstorming or research synthesis all benefit from an agent that can iterate on its own thinking.

If your process is predictable and linear, you probably want structured workflow patterns with explicit control flow instead. Agents are for when you need flexibility, persistence and autonomous decision-making.

Project Setup

Before we dive into code, let’s get your development environment ready. You’ll need Node.js version 22 or later and a Google Gemini API key. If you haven’t got a Gemini API key, you can get one free from Google AI Studio. Our tech stack includes the Vercel AI SDK v5 for agent orchestration, @clack/prompts for beautiful terminal interactions, and @ai-sdk/google for Gemini API integration.

I’ve prepared a GitHub repository with everything you need to follow along. Clone the repository and run pnpm install to install dependencies. You have to create a .env file with your GOOGLE_API_KEY, and you’re ready to go. The starter template is in the project/03-ai-sdk-agent directory and includes basic TypeScript configuration and all required dependencies—no faffing about with configuration.

Building the AI Search and Article Summarizer

We’re building an agent that behaves like a mini Perplexity: you ask a question, and it autonomously searches the web, reads relevant articles and delivers a comprehensive, sourced summary. The brilliance of this example is that it uses two provider-native tools built directly into Gemini. The google_search and url_context tools allow us to access web content and context without needing to write code to find links and crawl pages.

Here’s the user experience we’re creating: You type a question like “What are the latest developments in AI reasoning models?” The agent searches Google, evaluates which results look promising, fetches and analyzes those URLs, synthesizes the information and presents you with a summary, complete with source citations. All of this happens automatically without you orchestrating each step.

The agent decides how many searches to perform, which URLs to read and when it has enough information to answer confidently. That’s the power of autonomous behavior.

Introducing Experimental_Agent

The Experimental_Agent class is your high-level interface for building agents with the AI SDK. It handles everything we discussed earlier—the conversation loop, context management and stopping conditions—while giving you a clean, declarative API.

In v6 this class will likely be called simply Agent or ToolLoopAgent, but for now, the Experimental_Agent name signals that this is experimental functionality subject to change.

Why use Experimental_Agent instead of manually managing the loop with generateText()? Consider what you’d need to implement yourself: an array to store conversation history, a while loop that continues until some condition is met, maybe logic to append tool results to the history and stopping conditions to prevent infinite loops. That’s a lot of boilerplate code, and getting it wrong means buggy agents or runaway API costs.

Experimental_Agent does all of this for you while providing type safety and configurability. You define your agent once, and you can use it throughout your application with complete confidence that the loop mechanics are handled correctly.

Code Implementation

Let’s build this program step by step. We’ll start with the foundations and progressively add functionality until we have a complete, production-ready agent.

Initial Setup and Imports

First, we need our dependencies and some basic configuration. Create a new file called search-agent.ts:

import { Experimental_Agent as Agent, stepCountIs } from "ai";
import { google } from "@ai-sdk/google";
import * as prompts from "@clack/prompts";

// We'll use stepCountIs to limit the agent to a maximum number of steps
// This prevents runaway loops and controls costs
const MAX_AGENT_STEPS = 10;

These imports give us everything we need: Agent for building our agent, stepCountIs for controlling how long the agent can run, the Google provider for accessing Gemini’s models and tools, and Clack’s prompts for beautiful terminal interactions.

The MAX_AGENT_STEPS constant is important. Each “step” is one generation. Either the model generates text (completing the task) or it calls a tool (continuing the loop). By limiting steps, we keep the agent from running indefinitely if something goes wrong.

Configuring the Agent

Now for the heart of our application—defining the agent itself:

const searchAgent = new Agent({
  model: google("gemini-2.5-flash"),

  system: `You are an expert research assistant. When users ask questions:
  
1. Search the web to find relevant, recent information
2. Read the most promising sources using the url_context tool
3. Synthesise information from multiple sources
4. Provide a comprehensive answer with proper citations

Always aim to verify claims across multiple sources before presenting them as fact.
Keep your responses clear and well-structured.`,

  tools: {
    google_search: google.tools.googleSearch({}),
    url_context: google.tools.urlContext({}),
  },

  stopWhen: stepCountIs(MAX_AGENT_STEPS),
});

Let’s unpack this configuration.

The model is Gemini 2.5 Flash, which offers an excellent balance of speed, cost and capability for agent workflows. The system field is where we define the agent’s behaviour and strategy. Notice we’re not just saying “be helpful”; we’re giving it a specific method: search first, read sources, synthesize and cite. This guidance shapes how the agent approaches tasks.

The google_search tool lets the agent search Google and receive structured results, while the url_context tool allows it to fetch and understand the full content of any URL. Together, these tools give the agent genuine research capabilities without us writing a single line of web-scraping code.

Finally, stopWhen: stepCountIs(MAX_AGENT_STEPS) means the agent can’t exceed 10 steps. This is our safety net against infinite loops while still giving the agent enough runway to search, read multiple sources and synthesize findings.

Building the User Interface

Let’s create a clean terminal interface for interacting with our agent:

async function main() {
  prompts.intro(" AI Search and Article Summariser");

  console.log("\nAsk me anything, and I'll search the web for answers!");
  console.log('Type "exit" to quit.\n');

  while (true) {
    const userQuery = await prompts.text({
      message: "Your question:",
      placeholder: "e.g., What are the latest AI breakthroughs?",
      validate: (value) => {
        if (!value) return "Please enter a question";
      },
    });

    if (prompts.isCancel(userQuery)) {
      prompts.cancel("Search cancelled.");
      process.exit(0);
    }

    if (userQuery === "exit") {
      prompts.outro(" Thanks for using AI Search!");
      break;
    }

    // We'll handle the agent interaction here next
  }
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

We use @clack/prompts for a polished user experience in the terminal. The intro() call creates a nice header, we show usage instructions, and then we enter an infinite loop prompting for questions. The validate function means users can’t submit empty queries, and we handle both Ctrl+C cancellation and explicit “exit” commands gracefully.

Step 4: Processing Agent Responses

Now let’s connect the user interface to our agent and handle its responses:

// Inside the while loop, after getting userQuery:

const spinner = prompts.spinner();
spinner.start("Researching your question...");

try {
  const result = await searchAgent.generate({
    prompt: userQuery,
  });

  spinner.stop("Research complete!");

  // Display the agent's response
  prompts.note(result.text, "Answer");

  // Show some transparency about what the agent did
  console.log(`\n Research process: ${result.steps.length} steps taken\n`);
} catch (error) {
  spinner.stop("Error occurred");
  console.error("\n❌ Failed to process your question:", error);
  console.log("Let's try again...\n");
}

This is where the magic happens. We call searchAgent.generate() with the user’s question, and the agent takes over completely. It will search, read sources, iterate as needed and return when it has an answer or hits the step limit.

The result object contains several useful properties. The text field is the agent’s final response, i.e., the answer to the user’s question. The steps array contains a record of everything the agent did: which tools it called, what parameters it used and what results it received. This is invaluable for debugging and understanding agent behavior.

Notice we’re showing the step count to users. This transparency helps them understand that genuine research is happening behind the scenes. When they see "7 steps taken," they know the agent didn’t just guess—it actually searched, read sources and reasoned through the answer.

Complete Implementation

Here’s the full, production-ready code in one place. Save this as search-agent.ts if you haven’t already:

// Complete implementation - save as search-agent.ts
import { Experimental_Agent as Agent, stepCountIs } from "ai";
import { google } from "@ai-sdk/google";
import * as prompts from "@clack/prompts";

const MAX_AGENT_STEPS = 10;

const searchAgent = new Agent({
  model: google("gemini-2.5-flash"),

  system: `You are an expert research assistant. When users ask questions:

1. Search the web to find relevant, recent information
2. Read the most promising sources using the url_context tool
3. Synthesise information from multiple sources
4. Provide a comprehensive answer with proper citations

Always aim to verify claims across multiple sources before presenting them as fact.
Keep your responses clear and well-structured.`,

  tools: {
    google_search: google.tools.googleSearch({}),
    url_context: google.tools.urlContext({}),
  },

  stopWhen: stepCountIs(MAX_AGENT_STEPS),
});

async function main() {
  prompts.intro(" AI Search and Article Summariser");

  console.log("\nAsk me anything, and I'll search the web for answers!");
  console.log('Type "exit" to quit.\n');

  while (true) {
    const userQuery = await prompts.text({
      message: "Your question:",
      placeholder: "e.g., What are the latest AI breakthroughs?",
      validate: (value) => {
        if (!value) return "Please enter a question";
      },
    });

    if (prompts.isCancel(userQuery)) {
      prompts.cancel("Search cancelled.");
      process.exit(0);
    }

    if (userQuery === "exit") {
      prompts.outro(" Thanks for using AI Search!");
      break;
    }

    const spinner = prompts.spinner();
    spinner.start("Researching your question...");

    try {
      const result = await searchAgent.generate({
        prompt: userQuery,
      });

      spinner.stop("Research complete!");
      prompts.note(result.text, "Answer");
      console.log(
        `\n Research process: ${result.steps.length} steps taken\n`,
      );
    } catch (error) {
      spinner.stop("Error occurred");
      console.error("\n❌ Failed to process your question:", error);
      console.log("Let's try again...\n");
    }
  }
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

To run your agent, execute:

node --env-file=.env search-agent.ts

Let’s put your agent through its paces with some real-world queries that showcase its capabilities.

Query 1: Current Events

Try asking: “What are the top stories on Hacker News right now?”

The agent will search for Hacker News, use url_context to fetch the homepage, parse the trending stories and present you with a structured list.

Query 2: Technical Research

Ask: “What is the Model Context Protocol, and why does it matter for AI development?”

Query 3: Comparative Analysis

Try: “Compare the latest AI reasoning models from OpenAI and Google”

Here’s what a typical session looks like:

┌   AI Search and Article Summariser

Ask me anything, and I'll search the web for answers!
Type "exit" to quit.

◇  Your question:
│  What are the latest developments in AI reasoning models?
│
◆  Researching your question...
│
◇  Research complete!
│
◇  Answer ─────────────────────────────────────────────────────────
│
│  Recent developments in AI reasoning models show significant
│  progress across multiple fronts. OpenAI's o1 model series
│  introduces "chain of thought" reasoning that allows models to
│  spend more time thinking before responding, particularly
│  excelling in mathematics and coding challenges.
│
│  Google has announced Gemini 2.0, which features enhanced
│  reasoning capabilities and can now plan multi-step tasks
│  autonomously. The model shows particular strength in scientific
│  reasoning and complex problem decomposition.
│
│  A key trend is the shift from simply scaling model size to
│  improving inference-time compute—essentially teaching models
│  to "think longer" rather than just "know more." This approach
│  has yielded impressive results on benchmarks like GPQA and MATH.
│
│  Sources: OpenAI's technical blog (December 2024), Google AI
│  announcements, Nature journal coverage of reasoning model advances
│
├────────────────────────────────────────────────────────────────────

 Research process: 1 step taken

Key Takeaways and Next Steps

You’ve just built a useful AI search agent, and more importantly, you understand the fundamental shift from basic tool calling to autonomous agent behavior. Let’s crystallize the core lessons:

What an AI agent is: An agent is an LLM that uses tools repeatedly in a loop to autonomously accomplish complex tasks, managing its own workflow from start to finish rather than requiring you to orchestrate each step manually.

When to build one: Reach for agents when your task requires multi-step reasoning, adaptive decision-making or workflows where you can’t predict the exact sequence of actions up front. Things like research, debugging, creative problem-solving and exploratory analysis are perfect use cases.

How to build with AI SDK: The Experimental_Agent class encapsulates all the complexity of agent loops, giving you a declarative API where you focus on defining capabilities (tools) and behaviour (system instruction) while the SDK handles orchestration, context management and stopping conditions.

Now that you’ve mastered the fundamentals, here’s how you can extend this foundation:

Add custom tools to give your agent domain-specific capabilities. Following the patterns from our previous tool calling guide, you could add tools for querying your company’s database, sending emails or interfacing with any API. The agent would then weave these capabilities into its autonomous workflows.

Build specialized agents for different domains—a code review agent with tools for running tests and analysing security, a content agent that can research topics and draft articles, or a data-analysis agent that can query databases and generate visualisations. The pattern you’ve learned applies to any autonomous workflow.

The world of AI agents is evolving rapidly, and you’re now equipped to build genuinely capable systems. In future guides, we’ll explore multi-agent architectures where specialised agents collaborate, stop conditions for fine-grained control and streaming responses so users see the agent’s thinking in real time.

But for now, take what you’ve learned and build something remarkable. Get the source code from the GitHub repository.


Found this guide helpful? Share it with your developer friends and colleagues. If you have questions or want to share what you’ve built, I’d love to hear from you on Twitter or GitHub. Happy building!

Read the whole story
alvinashcraft
18 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Navigating Career Choices in Software Engineering

1 Share
Veteran Microsoft MVP Allen Conway brings over 20 years of experience designed to help developers navigate key career decisions -- from job changes to leadership vs. technical roles. Learn how to evaluate your career path with clarity and confidence.
Read the whole story
alvinashcraft
18 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Enabling multi-tenant authentication for MCP servers with Microsoft Entra and APIM

1 Share
Featured image of post Enabling multi-tenant authentication for MCP servers with Microsoft Entra and APIM

In my previous post, we explored how to build an authenticated MCP server using Microsoft Entra for identity management and Azure API Management to protect our APIs. That implementation worked great, but it had one important limitation: it only supported single-tenant authentication. This means that only users from the same organization where the app registration was created could access the MCP server.

While single-tenant authentication is perfectly fine for internal applications, there are many scenarios where you need to enable access to users from different organizations. For example, imagine you’re building an MCP server as an ISV that you want to offer to multiple customers. In these cases, requiring each user to create their own app registration becomes impractical and creates unnecessary friction.

In this post, we’re going to explore how to enable multi-tenant authentication for our MCP server. The good news is that the changes we need to make are surprisingly straightforward—they’re all in the Azure configuration, with no code changes required. However, knowing exactly which settings to change isn’t always obvious, so let’s walk through the process step by step.

Understanding the challenge

Before we dive into the configuration changes, it’s important to understand what we’re trying to solve. In the single-tenant setup from the previous post, both our app registrations (the API and the client) were configured to accept “Accounts in this organizational directory only.” This means that when Microsoft Entra validates an access token, it checks that the user belongs to the same tenant where the app was registered.

For multi-tenant scenarios, we need to make a few key changes:

  1. Configure both app registrations to accept accounts from any organizational directory
  2. Update the APIM policies to use the common endpoint instead of a specific tenant ID
  3. Add explicit client application IDs to the token validation policy
  4. Update the OAuth metadata endpoint to point to the multi-tenant endpoints

The beauty of this approach is that all these changes happen at the infrastructure level. Our .NET MCP server code doesn’t need to change at all, since authentication is handled entirely by APIM.

Let’s get started!

Updating the app registrations

The first step is to update both app registrations to support multi-tenant authentication. We need to modify both the API app registration (Flights MCP Server) and the client app registration (Flights MCP Client).

Updating the Flights MCP Server API app registration

Let’s start with the API app registration. Navigate to the Azure portal and go to the App registrations section. Find the Flights MCP Server app registration we created in the previous post.

  1. Click on the app registration to open its details
  2. In the left menu, click on Authentication and move to the Settings tab
  3. Under Supported account types, change the selection from Accounts in this organizational directory only to Accounts in any organizational directory (Any Microsoft Entra directory - Multitenant)
  4. Click Save to apply the changes

That’s it for the API app registration! Notice that we didn’t need to change anything else. The Application ID URI (api://your-client-id), the access_as_user scope, and all other settings remain exactly the same.

Configuring the app registration to support multi-tenant

Updating the Flights MCP Client app registration

Now we need to make the same change to the client app registration. Still in the App registrations section, find the Flights MCP Client app registration.

  1. Click on the app registration to open its details
  2. In the left menu, click on Authentication and move to the Settings tab
  3. Under Supported account types, change the selection to Accounts in any organizational directory (Any Microsoft Entra directory - Multitenant)
  4. Click Save to apply the changes

Again, the redirect URIs (https://vscode.dev/redirect and http://127.0.0.1:33418) and the API permissions (access_as_user from the Flights MCP Server API) remain unchanged.

Now that both app registrations are configured for multi-tenant support, we can move to the APIM configuration changes.

Updating the APIM token validation policy

The next step is to update the APIM policy that validates access tokens. This is where we need to make two important changes: switching to the common tenant ID and adding explicit client application IDs.

Navigate to your APIM instance in the Azure portal, then:

  1. Click on APIs in the left menu
  2. Select the API that was created for your MCP server
  3. Click on the Design tab
  4. Select All operations
  5. In the Inbound processing section, click on the policy editor icon (the </> symbol)

You should see the existing policy that we created in the previous post. Now we need to modify the <validate-azure-ad-token> element. Here’s what the updated policy looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<inbound>
 <base />
 <set-backend-service id="apim-generated-policy" backend-id="WebApp_flightsapismcp" />
 <validate-azure-ad-token tenant-id="common" header-name="Authorization" failed-validation-httpcode="401">
 <client-application-ids>
 <!-- Multi-tenant client app registration (shared with attendees) -->
 <application-id>your-client-app-id</application-id>
 </client-application-ids>
 <audiences>
 <!-- Your existing API app registration -->
 <audience>api://your-api-app-id</audience>
 <audience>your-api-app-id</audience>
 </audiences>
 </validate-azure-ad-token>
</inbound>

Let’s break down the two key changes:

Using the common tenant ID

The first change is replacing the specific tenant ID with common:

1
<validate-azure-ad-token tenant-id="common" ...>

The common endpoint is a special Microsoft Entra endpoint that accepts users from any organizational directory. When a user authenticates, Microsoft Entra will validate their credentials against their home tenant, but the token validation will work for any tenant.

Adding client application IDs

The second change is the addition of the <client-application-ids> section:

1
2
3
<client-application-ids>
 <application-id>your-client-app-id</application-id>
</client-application-ids>

This is a critical piece that isn’t immediately obvious. When using multi-tenant authentication, you need to explicitly specify which client applications are allowed to call your API. Replace your-client-app-id with the Application (client) ID of your Flights MCP Client app registration.

This is what enables the ISV scenario I mentioned earlier: you create one multi-tenant client app registration, and then you can share its client ID with all workshop attendees. They don’t need to create their own app registrations, they simply use the one you provide, and they authenticate with their own credentials from their own tenant.

Remember to replace your-api-app-id with the actual Application (client) ID of your Flights MCP Server app registration.

The updated inbound policy in APIM

Updating the OAuth metadata endpoint

You might remember that, in the previous post, we have added to our MCP server a special endpoint, /.well-known/oauth-authorization-server. This endpoint is used by clients (like Visual Studio Code) to discover the authorization and token endpoints they need to use for authentication. Now we need to update the OAuth metadata endpoint that we configured in the previous post to support a multi-tenant scenario.

Still in the APIM Design tab:

  1. Find the operation called OAuth 2.0 Authorization Server Metadata (the one at /.well-known/oauth-authorization-server)
  2. Click on it to select it
  3. In the Inbound processing section, click on the policy editor icon

You’ll see the policy that returns the OAuth metadata. We need to update all the URLs to use common instead of the specific tenant ID. Here’s the updated policy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<inbound>
 <return-response>
 <set-status code="200" reason="OK" />
 <set-header name="Content-Type" exists-action="override">
 <value>application/json</value>
 </set-header>
 <set-body>{
 "issuer": "https://login.microsoftonline.com/common/v2.0",
 "authorization_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
 "token_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
 "scopes_supported": ["api://your-api-app-id/.default"],
 "response_types_supported": ["code"],
 "code_challenge_methods_supported": ["S256"]
 }</set-body>
 </return-response>
</inbound>

Notice how all three endpoints (issuer, authorization_endpoint, and token_endpoint) now use common instead of a specific tenant ID. The scopes_supported array remains unchanged, it still references your API app registration.

Remember to replace your-api-app-id with the actual Application (client) ID of your Flights MCP Server app registration.

The OAuth authorization server endpoint in APIM

That’s it! Your APIM configuration is now ready for multi-tenant authentication.

Testing the multi-tenant setup

Now comes the moment of truth: testing that our multi-tenant configuration actually works. The best way to do this is to test with a user from a different tenant than the one where you created the app registrations.

Testing with the original tenant

Before testing with a different tenant, I recommend first verifying that your original setup still works. This is an important regression test! You want to make sure that users from your own tenant can still authenticate successfully.

Open Visual Studio Code and configure the mcp.json file to point to your APIM endpoint, just like you did in the previous post. When you start the MCP server connection, you should be prompted to authenticate. Log in with your original credentials (from the tenant where the app registrations were created), and verify that everything still works as expected. You should be able to still see, from the toolbar, that the MCP server is running and that there 4 tools available. You could also ask GitHub Copilot to find flights just like before.

Testing with a different tenant

Now for the real test: authenticating with a user from a different tenant. If you don’t have one and you are eligible, you can create a free Microsoft 365 developer tenant for testing purposes.

Here’s an important tip: Visual Studio Code caches authentication credentials, and I didn’t find an easy way to clear them without completely resetting the storage. For testing, I recommend using Visual Studio Code Insiders as a separate installation or a separate PC with another Visual Studio Code installation. This way, you can test the multi-tenant flow without interfering with your existing VS Code setup. If, by any chance, you want to clear the cached credentials in your existing VS Code installation, you can delete the files states.vscbd and states.vscdb.backup located in:

  • For VS Code: the %APPDATA%\Code\User\globalStorage\ folder on Windows
  • For VS Code Insiders: the %APPDATA%\Code - Insiders\User\globalStorage\ folder on Windows

Deleting these files will completely reset the VS Code configuration, including extensions, customizations, and settings. This is why I suggest using a separate installation for testing.

When you try to connect to the MCP server with a user from the new tenant:

  1. Visual Studio Code will prompt you for the client ID. Provide the Application (client) ID of your Flights MCP Client app registration
  2. When asked for a secret, just leave it empty and press Enter (since we’re using a public client)
  3. You’ll be redirected to the Microsoft Entra login page. Login with an account from your different tenant.
  4. After logging in, you’ll see a consent screen asking for permission to access the Flights MCP Server API
  5. Click Accept to grant consent

This consent step happens only the first time a user from a new tenant authenticates. Because we configured the access_as_user scope to allow user consent (not just admin consent), regular users can grant this permission themselves, with no admin involvement needed.

After consenting, you should see the MCP server connection established successfully. Try asking GitHub Copilot to find flights from New York to London, just like in the previous post. If everything works, you’ve successfully enabled multi-tenant authentication!

Troubleshooting tips

As I was implementing this multi-tenant setup, I ran into a few issues that might help you if you encounter similar problems.

Token validation errors

The most common error I encountered was a 404 (content not found) when trying to call the API. This wasn’t very helpful, as it didn’t clearly indicate that the problem was with token validation.

What really helped me troubleshoot was using the Azure CLI to generate a valid bearer token and then testing it directly in APIM. Here’s the command I used:

1
az account get-access-token --resource "api://your-api-app-id" --query accessToken -o tsv

Replace your-api-app-id with the actual Application (client) ID of your Flights MCP Server app registration. This command will return a bearer token that you can use for testing.

Once you have the token, go to your APIM instance in the Azure portal:

  1. Navigate to APIs and select your MCP server API
  2. Click on the Test tab
  3. Select the /mcp endpoint
  4. In the HTTP Request section, add an Authorization header with the value Bearer YOUR_TOKEN_HERE (replace YOUR_TOKEN_HERE with the token from the CLI command)
  5. Execute the test by pressing the Trace button

The operation will fail anyway because we aren’t supplying valid MCP parameters, but the Trace output will show you detailed information about the token validation process, so you can diagnose what’s going wrong. For example, during one of my tests, I discovered that the audience claim in the token wasn’t matching what the policy expected. Without the trace output, I would have been completely lost.

Security considerations

Before you implement multi-tenant authentication, it’s important to understand the security implications. With the configuration we just implemented, any user with a valid Microsoft Entra ID from any organization can authenticate and access your MCP server.

For my scenario (delivering a workshop where attendees are required to connect my protected MCP server from their own tenant), this is exactly what I wanted. I don’t know in advance which tenants my attendees will come from, so I need to accept any organizational account. However, this might not be appropriate for all scenarios.

I recommend reviewing the Azure Architecture Center’s guide on multi-tenancy with API Management for more detailed security considerations and best practices.

Wrapping up

In this post, we’ve learned how to enable multi-tenant authentication for MCP servers using Microsoft Entra and Azure API Management. The changes we made were surprisingly simple—just a few configuration updates in the app registrations and APIM policies—but they unlock powerful scenarios for sharing your MCP servers with users from different organizations.

To summarize, here are the four key changes needed to move from single-tenant to multi-tenant:

  1. Update both app registrations (API and client) to support “Accounts in any organizational directory”
  2. Modify the APIM token validation policy to use tenant-id="common" and add the <client-application-ids> section
  3. Update the OAuth metadata endpoint to use common in all URLs
  4. No code changes required in your .NET MCP server

This multi-tenant setup is perfect for scenarios like workshops, where you want to minimize friction for attendees, or for ISVs who want to offer their MCP servers as a service to multiple customers. Users from different organizations can all use the same shared client ID, and they authenticate with their own credentials from their own tenants.

The complete source code for the MCP server is still available in the same GitHub repository we used in the previous post—remember, the code itself didn’t change at all!

Happy coding!

Read the whole story
alvinashcraft
18 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories