Saturday, May 16, 2026

Squad with GitHub Copilot CLI: Human-led AI agent teams

What is Squad?

Squad is a team of agents that work on your behalf to conduct specializations like: DevOps, Testing, DB optimization, etc. Squad works with GitHub Copilot CLI. In this article, we will explore Squad by getting it to create ASP.NET Blazor application, then enhance it with more features.

ⓘ NOTE: 

  • This is an experimental project and may change over time.
  • Using Squad can result in the consumption of a sizable amount of AI tokens.

Pre-requisites

In order to proceed with this tutorial, you will need to have the following software installed on your computer:

  1. .NET 10.0 or later
  2. SQLite 
  3. GitHub Copilot CLI
  4. Node.js and npm (version 5.2.0 or higher) 

The GitHub repo for the Squad project is at https://github.com/bradygaster/squad

ⓘ NOTE the following about Squad:

  1. Squad works with GitHub Copilot
  2. You need to have Node.js and npm (version 5.2.0 or higher) installed on your computer in order to setup Squad.
  3. Using Squad can result in the consumption of a sizable amount of AI tokens.

Connect to a SQLite Database

Let's use GitHub Copilot CLI to connect to and explore the Chinook database (a sample database that represents a digital music store).

What is the Chinook Database?

The Chinook database models a digital media store, similar to an old iTunes store. It contains real music data and includes 11 tables:

Table Description
Artist Music artists
Album Albums released by artists
Track Individual songs, including price and duration
Genre Music genres (Rock, Jazz, Pop, etc.)
MediaType Format of the track (MP3, AAC, etc.)
Playlist Named playlists
PlaylistTrack Tracks belonging to each playlist
Customer Store customers
Employee Store employees and their reporting structure
Invoice Customer purchases
InvoiceLine Individual line items on each invoice

Connecting to the Database

  1. Download the Chinook.sqlite database file here 👉Click to download

  2. Create a folder named Chinook and place the downloaded Chinook.sqlite file inside it.

  3. Open your terminal, navigate to the Chinook folder, and launch GitHub Copilot CLI, by typing in:

copilot
        
  1. Wait for the Copilot CLI interface to load. You should see the prompt ready for input.

  2. Once inside Copilot CLI, type the following prompt and press ENTER to establish a connection to the Chinook database. This tells Copilot which database file to use and how to access it.

    Connect to the Chinook.sqlite database using the connection string DataSource=Chinook.sqlite;Cache=Shared;
            
  3. You should see Copilot confirm the connection. If you are asked to trust files in the current folder or asked to run commands, press ENTER to confirm Yes.

Exploring the Database

Once connected, try the following prompts one at a time. After each one, take a moment to look at the results before moving on.

List all tables in the database:

List all the tables in a table format
        

See what is inside a table:

Display data in the Genre table.
        

Ask for insights:

Analyse the data in the database and provide me with some interesting insights.
        

Use SQLite database with ASP.NET app

Let's use GitHub Copilot CLI to build a simple ASP.NET Razor Pages web application that reads and manages data from the Chinook database. You will do this step by step, one prompt at a time.

💡TIP: If you want GitHub Copilot to run commands without always asking for confirmation, enter the command /yolo, which stands for "You Only Live Once".1


Prompt 1 — Create the Project

Type the following prompt inside Copilot CLI and press ENTER:

Create a simple ASP.NET Razor Pages web application using .NET 10 in a folder named Chinook.Web. Do not add any database or authentication yet. 
💡TIP: Wait for Copilot to finish completely before moving on to the next prompt. Rushing to the next step before Copilot is done is the most common cause of errors in this tutorial.

Prompt 2 — Connect the Database

⚠️WARNING: The following prompt connects to your existing Chinook.sqlite database. Do not modify or delete the database file while Copilot is running, as this may cause your data to be lost.

Add Entity Framework Core SQLite to Chinook.Web. Connect it to the existing Chinook.sqlite in the parent folder using connection string "DataSource=../Chinook.sqlite;Cache=Shared;" in appsettings.Development.json. Do not overwrite or delete any existing data.

💡TIP: To verify the app is running correctly, open a new terminal window, navigate to the Chinook.Web folder, and run:

dotnet watch

Your browser should open automatically.


Prompt 3 — Scaffold CRUD Pages

Create a Genre model that matches the existing Genre table in Chinook.sqlite. The SQLite database uses singular table names (e.g. Genre, not Genres), so the model must include a [Table("Genre")] attribute from System.ComponentModel.DataAnnotations.Schema to prevent Entity Framework Core from pluralizing the table name. Scaffold full CRUD Razor Pages for Genre. Add a Genre link to the main navigation menu. 
💡TIP: Go back to the terminal running dotnet watch. Your app should reload automatically. Navigate to the /Genres page in your browser. You should see a list of genres loaded from the Chinook database.

Prompt 4 — Apply the Theme

Replace the default Bootstrap CSS in _Layout.cshtml with the Bootswatch Sketchy theme CDN link from https://bootswatch.com/sketchy/

Your app should now look noticeably different (hand-drawn style buttons and a unique font). Refresh http://localhost:5000/Genres to see the new theme applied.


Prompt 5 — Run the app 

Run the app 

If everything is OK, you should see the Genre list page populated with data from the Chinook database. Try adding, editing, and deleting a genre to confirm that full CRUD functionality is working.

Let's use Squad

Install Squad globally on your computer by typing the follwoing terminal window command:

npm install -g @bradygaster/squad-cli
        

In the Chinook.Web folder created in tutorial number 2 (CRUD App), initialize Squad with:

squad init
        

Start a GitHub Copilot CLI session by typing the following terminal window command:

copilot
        

You must be logged into GitHub in order to use Squad. Type the following command in the input field to login into GitHub:

login

Select GitHub.com by hitting ENTER on 1.

GitHub.com

A message is displayed that a code will be placed in the clipboard and your browser will be used for authentication once you press any key.

authenticate

Your default browser will open to the Device Activation page.

device-activation

Choose your preferred GitHub account then click on Continue.

one-time-code

Enter the one-time code that was given to you in the GitHub Copilot CLI, then click on Continue. Note that it will be different from the code in the image above.

authorize

Click on Authorize github.

mfa

You might be required to go through the multi-function authentication process. Once you are fully authenticated, you should received the below message in your browser:

congrats

We will choose the Squad agent to help us improve the Chinook.Web app. In the input field, type the following command to select an agent:

Choose the Squad agent.

squad-agent

It would be exhausing for the developer to approve every action that agents will be attempting to make. Autopilot mode is a behavioral state where Copilot executes multi-step tasks autonomously until completion. We will set the CLI to work in autopilot mode. Cycle through all the various modes with SHIFT TAB and select autopilot.

autopilot

Enter this prompt:

I like the disney universe of cartoon characters and would like to assign a squad to work on the Chinook.Web project.  
        Review the current state, propose new features, and discuss next steps. 
        

Accept the request for elevated permissions.

permission

💡TIP: If you enter command /tasks, you can observe the individual agents at work.

A long process of autonomous work will commence. Sit back, relax, and watch what is happening. Once everything settles down, you will receive a message that looks like this:

Next steps you requested:
        
           - Review current state: Try "Mickey, review the current project 
          architecture"
           - Propose features: Try "Team, what should we build next?" or "Mickey, 
          propose features for the Chinook database"
           - Discuss next steps: Ask "What features would make the most impact?" or 
          have Mickey analyze the Chinook schema and suggest CRUD pages for other 
          entities (Artists, Albums, Tracks, etc.)
        
          Quick commands to try:
        
           - "Mickey, review the project" — architecture analysis
           - "Team, add CRUD pages for Artists" — parallel fan-out to build a feature
           - "What's in the Chinook database?" — I can answer directly (no spawn 
          needed)
           - "Ralph, status" — check for any work in progress
        
          The team is fully configured, state files are in place, and .gitattributes
          is set up for clean cross-branch merges. Ready to work!
        

Look at the source code, you will notice that much has happened.

  1. A number of skills were added to the .copilot/skills folder:

skills

  1. Workflows were added to the .github/workflows folder:

workflows

  1. Under the .squad folder, in addition to other folders and files, there is an agents folder with our Disney workers:

workers

Read the charter for each of these workers. For example, Mickey's charter in the charter.md file is:

# Mickey — Lead
        
        > Architecture, scope, and quality — the one who sees the whole picture
        
        ## Identity
        
        - **Name:** Mickey
        - **Role:** Lead & Architect
        - **Expertise:** System design, code review, architectural patterns, .NET best practices
        - **Style:** Direct and decisive. Thinks big picture first, details second.
        
        ## What I Own
        
        - Overall project architecture and design decisions
        - Code review and quality gates
        - Technical scope definition and feature planning
        - Cross-module integration and consistency
        
        ## How I Work
        
        - Start with the why, then the what, then the how
        - Push back on scope creep and unnecessary complexity
        - Review others' work with an architectural lens
        - Document key decisions in the team knowledge base
        
        ## Boundaries
        
        **I handle:** Architecture, design reviews, scope decisions, technical leadership
        
        **I don't handle:** Deep implementation details (that's for the specialists), day-to-day bug fixes
        
        **When I'm unsure:** I consult with the appropriate specialist (Donald for backend, Minnie for frontend, Goofy for testing strategy)
        
        **If I review others' work:** On rejection, I may require a different agent to revise (not the original author) or request a new specialist be spawned. The Coordinator enforces this.
        
        ## Model
        
        - **Preferred:** auto
        - **Rationale:** Coordinator selects the best model based on task type — cost first unless writing code
        - **Fallback:** Standard chain — the coordinator handles fallback automatically
        
        ## Collaboration
        
        Before starting work, use the `TEAM ROOT` provided in the spawn prompt. All `.squad/` paths must be resolved relative to this root.
        
        Before starting work, read `.squad/decisions.md` for team decisions that affect me.
        After making a decision others should know, write it to `.squad/decisions/inbox/mickey-{brief-slug}.md` — the Scribe will merge it.
        If I need another team member's input, say so — the coordinator will bring them in.
        
        ## Voice
        
        Opinionated about clean architecture. Will push back on technical debt. Prefers simplicity over cleverness. Thinks in systems, not just features. Not afraid to say "we shouldn't build that."
        

Go ahead and ask for more features. I asked for the following enhancements:

  1. add CRUD pages for Artists
  2. add Sales Dashboard & Reporting
  3. recruit a GitHub DevOps engineer to configure some github actions for CI
  4. add web designer to help make the UI of the entire web app more colorful and compelling
ⓘ NOTE that agents get to choose different models for theie assigned tasks. For example: Mickey is using claude-sonnet-4.6, and Daisy is using claude-opus-4.5, etc.

models

There are instances when one agent waits for other agents to complete their assigned tasks.

wait

The end result is that we now have a web app that is colorful, has artists crud, and a dashboard.

end-result

Here's what the dashboard looks like:

dashboard

To find out token usage, you can type the /usage command. I used 7.5 million tokens. Most were used in understanding the entireity of the code base.

usage

Squad is a very interesting tool and provides us with an insight into the future world of software development.

Friday, May 15, 2026

Getting started with GitHub Copilot CLI

GitHub Copilot CLI is a terminal-native AI coding assistant that brings the capabilities of GitHub Copilot directly into your command line. In this tutorial, I will help you install the product and take it for a quick spin.

In order to proceed, you will need the following:

  1. GitHub account
  2. If your operating system is Windows, you will need the latest version of Poweshell.

GitHub account

If you do not already have a GitHub account, create one at https://github.com.

PowerShell (only for Windows)

If you are using macOS or Linux, you can skip this section. Otherwise, if you are on Windows, you must ensure that you have PowerShell version 7 (or later) installed on your computer.

To determine the version of PowerShell on your Windows computer, start PowerShell, then enter the following command in the window:

$PSVersionTable.PSVersion
        

If your version is older than 7.0, then update PowerShell to the latest version by typing this command in a PowerShell terminal window:

winget install Microsoft.Powershell
        

To ensure that you have the version 7 (or later) of PowerShell, type the following in a terminal window:

pwsh
        

Once you have installed all the above, you're all set and ready to go.

GitHub Copilot CLI

You can find out whether or not you have the GitHub Copilot CLI by typing the following command in a terminal window:

copilot --version
        

If you do not have GitHub Copilot CLI, then visit the 'Installing GitHub Copilot CLI' site and follow the instructions for your operating system.

Using GitHub Copilot CLI

To start a GitHub Copilot CLI session, type the following command in a terminal window:

copilot
        

If asked to trust files in current folder, select Yes and press ENTER.

Confirm Trust

If this is the first time using GitHub Copilot CLI, you must login into GitHub by typing in the /login command followed by ENTER:

              login

Select GitHub.com by hitting ENTER on 1.

select github

A one-time code will displayed and your browser will be used for authentication once you press any key.

              one-time code

Your default browser will open to the Device Activation page.

Enter the one-time code that was given to you in the GitHub Copilot CLI, then click on Continue. Note that it will be different from the code in the image below.

Click on Authorize github. 

Once you are fully authenticated, you should receive the message in your browser:

  enter code

Go back to the copilot CLI and you will be asked to sign up for Copilot Free if you havent already. Select 1. Yes, sign up for Copilot Free and hit 'Enter'

enter code

Once you have completed everything, you can enter any prompt into the CLI. For example, enter the following:

What are the top five spoken languages?     

You might be asked an additional question. Choose whichever option you prefer.

enter code

This is the response I received:

enter code

NOTE: The AI model that you are using is in the bottom right corner of the tool.

To change the AI model, type /model followed by ENTER:

              enter code

You can switch to another AI model by using the up and down arrows on your keyboard. But for now we will stick with GPT-5 mini (default).

              enter code

Select Medium for the reasoning effort for GPT-5 mini

enter code

To quit GitHub Copilot CLI and return to the host operating system, type /exit followed by ENTER.

Thursday, March 12, 2026

Function Calling with Microsoft Agent Framework, C#, & Entity Framework

In this article, we will create a Microsoft Agentic Framework plugin that contains four functions that interact with live SQLite data. Entity Framework will be used to access the SQLite database. The end result is to use the powers of the OpenAI natural language models to ask questions and get answers about our custom data.

Source Code: https://github.com/medhatelmasry/EfFuncCallMAF

Pre-requisites

  • You will be using AI models hosted on GitHub. Therefore, you will need to obtain a personal access token from GitHub.
  • .NET Framework 10.0+

Getting Started

Let’s start by creating an ASP.NET Razor pages web application. Select a suitable working folder on your computer, then enter the following terminal window commands:

dotnet new razor --auth individual -o EfFuncCallMAF
cd EfFuncCallMAF

Te above creates a Razor Pages app with support for Entity Framework and SQLite.

Add these packages:

dotnet add package CsvHelper
dotnet add package Microsoft.Agents.AI.OpenAI --prerelease 
dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI
dotnet add package Microsoft.EntityFrameworkCore.Sqlite

The CsvHelper package will help us load a list of products from a CSV file named students.csv and hydrate a list of Student objects. The second package is needed to work with Microsoft Agent Framework. The rest of the packages support Identity, Entity Framework and SQLite.

Let’s Code

appsettings.json

Add these to appsettings.json:

"GitHub": {
  "Token": "PUT-GITHUB-PERSONAL-ACCESS-TOKEN-HERE",
  "ApiEndpoint": "https://models.github.ai/inference",
  "Model": "openai/gpt-4o-mini"
}

Of course, you need to adjust the Token setting with your GitHub personal access token.

Data

Create a folder named Models. Inside the Models folder, add the following Student class: 

public class Student {
   public int StudentId { get; set; }

   [Display(Name = "First Name")]
   [Required]
   public string? FirstName { get; set; }

   [Display(Name = "Last Name")]
   [Required]
   public string? LastName { get; set; }

   [Required]
   public string? School { get; set; }
 
   public override string ToString() {
      return $"Student ID: {StudentId}, First Name: {FirstName}, Last Name: {LastName}, School: {School}";
   }
}

Developers like having sample data when building data driven applications. Therefore, we will create sample data to ensure that our application behaves as expected. Copy CSV data from this link and save it to a text file wwwroot/students.csv.

Add the following code inside the Data/ApplicationDbContext class located inside the Data folder:

public DbSet<Student> Students => Set<Student>();    
 
protected override void OnModelCreating(ModelBuilder modelBuilder) {
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<Student>().HasData(LoadStudents());
}  
 
// Load students from a csv file named students.csv in the wwwroot folder
public static List<Student> LoadStudents() {
    var students = new List<Student>();
    using (var reader = new StreamReader(Path.Combine("wwwroot", "students.csv"))) {
        using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
        students = csv.GetRecords<Student>().ToList();
    }
    return students;
}

Let us add a migration and subsequently update the database. Execute the following CLI commands in a terminal window.

dotnet ef migrations add M1 -o Data/Migrations
dotnet ef database update

At this point the database and tables are created in a SQLite database named app.db.

Helper Methods

We need a couple of static helper methods to assist us along the way. In the Models folder, add a class named Utils and add to it the following class definition:

public class Utils {
  public static string GetConfigValue(string config) {
    IConfigurationBuilder builder = new ConfigurationBuilder();
    if (System.IO.File.Exists("appsettings.json"))
      builder.AddJsonFile("appsettings.json", false, true);
    if (System.IO.File.Exists("appsettings.Development.json"))
      builder.AddJsonFile("appsettings.Development.json", false, true);
    IConfigurationRoot root = builder.Build();
    return root[config]!;
  }
 
  public static ApplicationDbContext GetDbContext() {
    var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
    var connStr = Utils.GetConfigValue("ConnectionStrings:DefaultConnection");
    optionsBuilder.UseSqlite(connStr);
    ApplicationDbContext db = new ApplicationDbContext(optionsBuilder.Options);
    return db;
  }
}

Method GetConfigValue() will read values in appsettings.json from any static method. The second GetDbContext() method gets an instance of the ApplicationDbContext class, also from any static method.

Plugins

Create a folder named Plugins and add to it the following class file named StudentPlugin.cs with this code:

public class StudentPlugin {
  [Description("Get student details by first name and last name")]
  public static string? GetStudentDetails(
    [Description("student first name, e.g. Kim")]
    string firstName,
    [Description("student last name, e.g. Ash")]
    string lastName
  ) {
      var db = Utils.GetDbContext();
      var studentDetails = db.Students
        .Where(s => s.FirstName == firstName && s.LastName == lastName).FirstOrDefault();
      if (studentDetails == null)
          return null;
      return studentDetails.ToString();
  }

  [Description("Get students in a school given the school name")]
  public static string? GetStudentsBySchool(
  [Description("The school name, e.g. Nursing")]
  string school
  ) {
      var studentsBySchool = Utils.GetDbContext().Students
        .Where(s => s.School == school).ToList();
      if (studentsBySchool.Count == 0)
          return null;
      return JsonSerializer.Serialize(studentsBySchool);
  }


  [Description("Get the school with most or least students. Takes boolean argument with true for most and false for least.")]
  static public string? GetSchoolWithMostOrLeastStudents(
  [Description("isMost is a boolean argument with true for most and false for least. Default is true.")]
  bool isMost = true
  ) {
      var students = Utils.GetDbContext().Students.ToList();
      IGrouping<string, Student>? schoolGroup = null;
      if (isMost)
          schoolGroup = students.GroupBy(s => s.School)
              .OrderByDescending(g => g.Count()).FirstOrDefault()!;
      else
          schoolGroup = students.GroupBy(s => s.School)
              .OrderBy(g => g.Count()).FirstOrDefault()!;
      if (schoolGroup != null)
          return $"{schoolGroup.Key} has {schoolGroup.Count()} students";
      else
          return null;
  }

  [Description("Get students grouped by school.")]
  static public string? GetStudentsInSchool() {
      var students = Utils.GetDbContext().Students.ToList().GroupBy(s => s.School)
        .OrderByDescending(g => g.Count());
      if (students == null)
          return null;
      else
          return JsonSerializer.Serialize(students);
  }
}

 In the above code, there are four methods with these purposes:

GetStudentDetails()Gets student details given first and last names
GetStudentsBySchool()Gets students in a school given the name of the school
GetSchoolWithMostOrLeastStudents()Takes a Boolean value isMost – true returns school with most students and false returns school with least students.
GetStudentsInSchool()Takes no arguments and returns a count of students by school.

Registering the Chat Client

In the Program.cs file, add the following code to register the MAF chat client so it is available for dependency injecton. The code goes before the "var app = builder.Build();" statement.

string? apiKey = builder.Configuration["GitHub:Token"];
string? model = builder.Configuration["GitHub:Model"] ?? "openai/gpt-4o-mini";
string? endpoint = builder.Configuration["GitHub:ApiEndpoint"] ?? "https://models.github.ai/inference";

builder.Services.AddSingleton<IChatClient>(_ =>
    new OpenAIClient(
        new ApiKeyCredential(apiKey!),
        new OpenAIClientOptions { Endpoint = new Uri(endpoint!) }
    ).GetChatClient(model!).AsIChatClient()
);

The User Interface

We will re-purpose the Index.cshtml and Index.cshtml.cs files so the user can enter a prompt in natural language and receive a response that comes from the OpenAI model working with our Microsoft Agent Framework plugin. 

Index.chtml.cs

Replace the IndexModel class definition in Pages/Index.cshtml.cs with:

public class IndexModel : PageModel {
  private readonly ILogger<IndexModel> _logger;
  private readonly IChatClient _chatClient;

  [BindProperty]
  public string? Reply { get; set; }

  public IndexModel(ILogger<IndexModel> logger, IChatClient chatClient) {
    _logger = logger;
    _chatClient = chatClient;
  }
  public void OnGet() { }
  // action method that receives prompt from the form
  public async Task<IActionResult> OnPostAsync(string prompt) {
    var response = await CallFunction(prompt);
    Reply = response;
    return Page();
  }

  private async Task<string> CallFunction(string question) {
    // Create tools from StudentPlugin methods
    var tools = new List<AITool> {
      AIFunctionFactory.Create(StudentPlugin.GetStudentDetails),
      AIFunctionFactory.Create(StudentPlugin.GetStudentsBySchool),
      AIFunctionFactory.Create(StudentPlugin.GetSchoolWithMostOrLeastStudents),
      AIFunctionFactory.Create(StudentPlugin.GetStudentsInSchool),
    };

    // Create the AI agent with tools
    var agent = _chatClient.AsAIAgent(
      instructions: "You are a helpful assistant that can look up student information.",
      name: "StudentAgent",
      tools: tools
    );

    // Run streaming and collect the response
    string fullMessage = "";
    await foreach (var update in agent.RunStreamingAsync(question)) {
      if (!string.IsNullOrEmpty(update.Text)) {
        fullMessage += update.Text;
      }
    }
    return fullMessage;
  }
}

In the above code, the prompt entered by the user is posted to the OnPostAsync() method. The prompt is then passed to the CallFunction() method, which returns the final response from Azure OpenAI.

The CallFunction() method sets up the AI agent with tools.

Note that the IChatClient object is available through dependency injection

All the tools (or plugins) are loaded into a list of AITool objects.

Index.chtml

Replace the content of Pages/Index.cshtml with:

@page
@model IndexModel
@{
    ViewData["Title"] = "Function Calling with Microsoft Agent Framework";
}
<div class="text-center">
    <h3 class="display-6">@ViewData["Title"]</h3>
    <form method="post">
        <input type="text" name="prompt" size="80" required />
        <input type="submit" value="Submit" />
    </form>
    <div style="text-align: left">
        <h5>Example prompts:</h5>
        <p>Which school does Mat Tan go to?</p>
        <p>Which school has the most students?</p>
        <p>Which school has the least students?</p>
        <p>Get the count of students in each school.</p>
        <p>How many students are there in the school of Mining?</p>
        <p>What is the ID of Jan Fry and which school does she go to?</p>
        <p>Which students belong to the school of Business? Respond only in JSON format.</p>
        <p>Which students in the school of Nursing have their first or last name start with the letter 'J'?</p>
    </div>
    @if (Model.Reply != null)
    {
        <p class="alert alert-success" id="reply">@Model.Reply</p>
    }
</div>

The above markup displays an HTML form that accepts a prompt from a user. The prompt is then submitted to the server and the response is displayed in a paragraph (<p> tag) with a green background (Bootstrap class alert-success).

Meantime, at the bottom of the page there are some suggested prompts to facilitate testing – namely:

Which school does Mat Tan go to?
Which school has the most students?
Which school has the least students?
Get the count of students in each school.
How many students are there in the school of Mining?
What is the ID of Jan Fry and which school does she go to?
Which students belong to the school of Business? Respond only in JSON format.
Which students in the school of Nursing have their first or last name start with the letter 'J'?

Trying the application

In a terminal window, at the root of the Razor Pages web application, enter the following command:

dotnet watch

The following page will display in your default browser:

You can enter any of the suggested prompts to ensure we are getting the proper results. I entered the last prompt and got these results:


Conclusion

We have seen how The Micrsoft Agenr Framework and Function Calling can be used with data coming from a database. In this example we are using SQLite. However, any other database can be used using the same technique.

Monday, March 9, 2026

MCP server with Laravel

In this article, we will create a tool in our Laravel MCP server to add to-do items. We will then consume the MCP server from an MCP client like VS Code.

Source Code: https://github.com/medhatelmasry/mcp-todo-laravel

Getting Started

Create a standard Laravel app with the following command:

composer create-project laravel/laravel mcp-todo-laravel
cd mcp-todo-laravel

Install the official Laravel MCP package via Composer:

composer require laravel/mcp

Publish the MCP configuration and routing files:

php artisan vendor:publish --tag=ai-routes

This creates routes/ai.php, where we will register your MCP servers.

Open your application in VS Code. 

We need to register the ai.php in the application bootstrap file. Edit the bootstrap/app.php file. Make these changes:

1) Add this at the top:

use Illuminate\Support\Facades\Route;

2) Add this code just under “health: '/up',”:

then: function () {
  Route::prefix('mcp')
    ->middleware('api') // api middleware disables CSRF checks
    ->group(base_path('routes/ai.php'));
},


Create and Register the Server 

Generate an MCP server class to group our tools:

php artisan make:mcp-server TodoServer

This creates the following file:

<?php

namespace App\Mcp\Servers;

use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;

#[Name(Todo Server')]
#[Version('0.0.1')]
#[Instructions('This server is used to manage the to-do list. Tools are used to add to-do items.')]
class TodoServer extends Server {
    protected array $tools = [
        //
    ];

    protected array $resources = [
        //
    ];

    protected array $prompts = [
        //
    ];
}

Update the file with the text highlighted in yellow above.

Then, register it in routes/ai.php as either a local or web server (or both) by replacing the content with the following:


<?php
use App\Mcp\Servers\TodoServer;
use Laravel\Mcp\Facades\Mcp;

// Web server: accessible via HTTP POST at /mcp/todo
Mcp::web('/mcp/todo', TodoServer::class);

// Local server: runs as an Artisan command
Mcp::local('todo', TodoServer::class);


Create an MCP Tool 

Tools define the actions an AI can perform. Generate a new tool class with:

php artisan make:mcp-tool AddTodoTool

This adds the following tool:


<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('A description of what this tool does.')]
class AddTodoTool extends Tool {
    public function handle(Request $request): Response {
        //
        return Response::text('The content generated by the tool.');
    }
    public function schema(JsonSchema $schema): array {
        return [
            //
        ];
    }
}

Replace the above file with the following code:

<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('Add a to-do item to the database. It takes description, isDone (Boolean) and date as parameters.')]
class AddTodoTool extends Tool {
    public function handle(Request $request): Response {
        $validated = $request->validate( 
            [
                'description' => 'required|string',
                'isDone' => 'required|boolean',
                'created_at' => 'required|string',
            ]);

        logger($validated);

        return Response::text('The content generated by the tool.');
    }

    public function schema(JsonSchema $schema): array {
        return [
            'description' => $schema->string()
                ->description("The description of the to-do item")
                ->required(),
            'isDone' => $schema->boolean()
                ->description("True if done and False if not done")
                ->required(),
            'created_at' => $schema->string()
                ->description("The date when the to-do item was created")
                ->required(),
        ];
    }
}

Register the above tool in the $tools array in app/Mcp/Servers/TodoServer.php. Add the code highlighted in yellow below.

use App\Mcp\Tools\AddTodoTool;

. . . . . . . . . .

protected array $tools = [
    AddTodoTool::class,
];

Let’s now run the server with:

php artisan serve


Connecting to a Client

To test our server, we can use the MCP Inspector or add the endpoint to an AI IDE like VS Code. 

Let us first test with the MCP Inspector. In another terminal window from the same folder, start the MCP inspector with:

php artisan mcp:inspector mcp/todo

The inspector will open in your default browsesr. Add :8000 to the URL, then click on the black Connect button.

Click on Tools, followed by "List Tools".

Click on "Add Todo Tool", enter a description (like: Add to-do item: go to the gym), enter a date, then click on the "Run Tool" button - as shown below.

The following confirmation will be displayed:

We now know that the MCP server and to-do tool are working as expected. Let's consume the MCP service from VS Code. In Visual Studio Code, from the top menu, select View >> Pallette

Choose "MCP: Add Server ...".

Choose "HTTP (HTTP or Server-Sent Events).

Enter http://localhost:8000/mcp/todo for the URL, then hit ENTER.

Give the server ID: todo-mcp-server.

Choose: Workspace Available in the workspace, runs locally.

A file gets created under .vscode named mcp.json that looks like this:


{
	"servers": {
		"todo-mcp-server": {
			"url": "http://localhost:8000/mcp/todo",
			"type": "http"
		}
	},
	"inputs": []
}

Make sure the server is running.

In Visual Studio Code, open the chat panel by clicking on the below tool.

Choose Agent mode.


Next, click on the "Configure Tools ..." tool as shown below.

The "Configure Tools ..." panel will open at the top and you should see that our todo-mcp-server is running. 

Close the "Configure Tools ..." panel by clicking on the blue OK button.

Enter the following prompt into the chat windows:

using the todo mcp server, add this todo item: fix the dish washer dated 2026-03-07

VS Code will seek your permission to proceed. Click on the "Allow in this Session" button.

Sample Output:

Extending the Laravel MCP Tool

Let us get our Todo tool to add items into a SQLite database. We can get AI to do some of the coding for us. Enter this prompt into the chat window:
Create a model called Todo, Add a migration. The database table should have description (string) and isDone ( Boolean). In the model, add all columns as fillable.

This may request that you agree to the execution of this terminal window command:

It then asks you to run this command to apply the migration:

php artisan migrate

Go ahead and run the above command. 

In app/Mcp/Tools/AddTodoTool.php:

1) Add this at the top:

use App\Models\Todo;

2) Add this code to the handle() method just before the final return statement:

Todo::create($validated);

Test it out by entering this prompt:

I need to put gas in the car and buy bread.

Sample Result:

The todos table in the database/database.sqlite database file will contain the data that was just inserted into the database.

Conclusion

We have successfully added a Todo MCP tool in a Laravel application. There are so many opportunities to get AI to participate in a new way of concievinng applications that users can interface with using a AI chat interaction.