Featured image of post Using Model Context Protocol in agents - Copilot Studio

Using Model Context Protocol in agents - Copilot Studio

In this post, we'll see how to use the Model Context Protocol (MCP) with agents created with Copilot Studio.

In the previous post, we introduced the Model Context Protocol (MCP) and we’ve learned how platforms can use it to expose services to AI applications. You can think of it like an API platform, but optimized for AI applications: data output is optimized for LLM consumption; every service and its input are carefully described in natural language; etc. We have also built a simple MCP server that simulates a service to track vacation requests from employees. In the post, we were able to test our implementation thanks to Visual Studio Code and the recent MCP support that was added to GitHub Copilot. However, it wasn’t a very realistic scenario: I don’t think many people are really interested in using GitHub Copilot to request days off to their employer 😊. In this post, we’ll make the MCP server a bit more useful by enabling an agent to use it. Today, we will focus on the low code experience with Copilot Studio, while in the next post we’ll explore the pro-code experience with Semantic Kernel.

Implementing the SSE transport protocol

Copilot Studio has recently added preview support for MCP using the SSE transport protocol, which is a way to stream data over HTTP. In such a scenario, in fact, we can’t use the stdio approach we used in the previous post, since the client doesn’t run locally (like in the case of Visual Studio Code), but it’s an agent in the cloud. However, SSE is scheduled for deprecation in the latest MCP specifications and replaced by HTTP Streaming. The good news is that this new protocol is backward compatible with SSE, so the code we’re going to write today will work with both protocols, enabling Copilot Studio to use it as well.

Let’s move forward and reimplement the MCP server we built to use the HTTP Streaming / SSE transport protocol. The boilerplate code to manage and track vacation days request will be the same, and also the implementation of the class that exposes the functions as MCP tools. However, we’ll need to slightly change the underline architecture, since HTTP Streaming / SSE work in a different way than stdio. Being based on HTTP, we need a real server which is up & running and capable of managing the requests from the agents. As such, the MCP SDK for C# includes a few helpers that you can use to turn an ASP.NET Web application into an MCP server.

Let’s start by creating a new ASP.NET Web application. You can do this using the command line or Visual Studio. In both cases, we can use the ASP.NET Core Empty template, since we won’t need any of the default features that come with the web or API template. If you’re using the command line, you can run the following command:

1
dotnet new web -n MCP.SSE.Server

As the first step, we must also add to this project the MCP SDK for C# and the Azure Table SDK, which are available as a NuGet packages. You can do this using the command line or Visual Studio. If you’re using the command line, you can run the following commands:

1
2
dotnet add package ModelContextProtocol --prerelease
dotnet add package Azure.Data.Tables

Now make sure to copy all the classes we created in the previous post to manage the MCP tools and the vacation requests into this new project:

  • Employee.cs and EmployeeVacationEntity.cs inside the Entities folder. These classes map the employees and the vacation requests tracked in the Azure Table Storage.
  • EmployeeVacationService.cs and IEmployeeVacationService.cs inside the Services folder. These classes expose the methods to manage the vacation requests.
  • EmployeeVacationTool.cs. This class exposes the methods to manage the vacation requests as MCP tools.

Now we’re ready to implement the MCP server using the SSE transport protocol. This operation is really easy to do thanks to some helpers included in the MCP SDK for C#. Just open the Program.cs file and, before builder.Build() is called, add the following code:

1
2
3
4
builder.Services
    .AddMcpServer()
    .WithHttpTransport()
    .WithTools<EmployeeVacationTool>();

The three methods are used, in order, to:

  1. Add the MCP server to the ASP.NET Core pipeline.
  2. Configure the server to use the HTTP transport protocol.
  3. Register the tools that we want to expose to the agents. In this case, we are exposing the EmployeeVacationTool class, which contains the methods to manage the vacation requests.

Then, before calling the app.Run() method, add the following code to register the MCP endpoints:

1
2
3
app.MapMcp();

app.Run();

That’s it! Now, we’re still missing some code, which is the same one we wrote in the previous post to register the EmployeeVacationService class in the dependency injection container and to initialize the database with some test data in case it’s empty.This is how the final Program.cs file should look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using MCP.SSE.Server.Services;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Server;
using ModelContextProtocol.Utils.Json;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddSingleton<IEmployeeVacationService>(sp =>
    {
        var connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:tableServiceConnectionName");
        var tableName = "VacationsTable";
        return new EmployeeVacationService(connectionString, tableName, sp.GetRequiredService<ILogger<EmployeeVacationService>>());
    })
    .AddMcpServer()
    .WithHttpTransport()
    .WithTools<EmployeeVacationTool>();


var app = builder.Build();

// Seed fake employees if table is empty
using (var scope = app.Services.CreateScope())
{
    var vacationService = scope.ServiceProvider.GetRequiredService<IEmployeeVacationService>() as EmployeeVacationService;
    if (vacationService != null && await vacationService.IsTableEmptyAsync())
    {
        await vacationService.SeedFakeEmployeesAsync();
    }
}

app.MapMcp();

app.Run();

Before testing the server, we need to add the connection string for Azure Table to the appsettings.json file. You can do this by adding the following snippet:

1
2
3
4
5
{
  "ConnectionStrings": {
    "tableServiceConnectionName": "UseDevelopmentStorage=true"
  }
}

This is the connection string to use Azurite, the local emulator for Azure Storage.

Now we’re ready to run the server. Before doing that, make sure to run the Azurite Table Service emulator, like we did in the previous post:

  • You can click on the [Azurite Table Service] label in the Visual Studio Code application bar.
  • You can open the command palette (Ctrl+Shift+P) and run the Azurite: Start Table Service command.

Now press F5 to launch the debugging experience. If everything goes well, you should see the server starting up and a new browser opening up on the URL http://localhost:5248/. You will just see a message like the following one:

1
2
event: endpoint
data: message?sessionId=-JFYOh9dO5GvqMxhhTXfMg

This means that the MCP server is up and running. In case the client connects using the HTTP Streaming protocol, all the GET and POST operations will be managed by the same endpoint, which is the root of the server. To support SSE, instead, the server must expose two different endpoints:

  • /sse, which supports a GET request to establish the connection
  • /message, which supports a POST request to send messages to the server

You can see that the MCP SDK for C# provides backward compatibility with the SSE protocol by invoking the URL http://localhost:5248/sse in your browser. You should see the same exact response as before, meaning that the SSE transport protocol is working as expected.

Using the MCP server with Copilot Studio

To expose the MCP server to Copilot Studio, we need first to create a dev tunnel between our local machine and the cloud. Copilot Studio runs in the cloud and cannot directly connect to localhost. Thanks to a dev tunnel, we can test our MCP server implementation without deploying it to the cloud. In Visual Studio Code, you can create a dev tunnel by clicking on the Ports tab in the bottom panel:

The ports tab in Visual Studio Code

Click on the Forward a Port button and enter 5248 as port number. Visual Studio Code will create a public URL that points to http://localhost:5248. Before using it, however, we need to make the URL public, since by default Visual Studio Code creates private tunnels. Right click on the URL, choose Port Visibility and select Public. You will see a message asking to confirm, click Continue and notice how the Visibility column should be set to Public now.

The forwarded address

Now that we have a public URL, we are ready to move to Copilot Studio. As the first step, open the Copilot Studio portal. Copilot Studio enables MCP support through custom connectors. Power Platform has a catalog of 1000+ connectors, which enables makers to build agents that can connects to a wide range of platforms and services from Microsoft and 3rd parties, like Outlook, Excel, SharePoint, ServiceNow, SAP, etc. In case you need to connect to a service that is not available in the catalog (like an internal LOB application), you can create a custom connector. This is exactly what we need to do in this case, since Copilot Studio offers MCP support through a special custom connector.

Custom connectors aren’t immediately visible in the portal. The easiest way to find them is to open the Power Apps portal and, in the left panel, click on More, then press Discover all.

Click on More in the Power Apps portal

You will land on a page titled Discover all you can do with Power Apps. Scroll down until you see a box titled Data and click on the Custom connectors option on it.

Tip: you can click on the Pin icon on the left to pin the custom connectors option to the left panel, so that you can easily access it in the future.

The custom connector option

In this page, you will see a list of all the custom connectors you have created (or an empty page, in case you have none). Click on the + New custom connector button at the top right and select Create from blank. Give it a meaningful name, like MCP Vacation Requests and click on Continue. You will be redirected to the wizard to create the custom connector. However, in our case we’re going to ignore it, since we’re going to leverage the Swagger editor to prove an OpenAPI definition of the connector. As such, click on the Swagger editor switch in the top bar.

The Swagger editor

OpenAPI is a standard format (based in JSON or YAML) that you can use to describe an API. For our own purpose, we can use a standard definition, since the SSE transport protocol doesn’t have a dependency on the tools we expose. As we have learned in the beginning of the post, an MCP server which uses the SSE transport protocol always exposes two endpoints: /sse and /message. It will be up to client to call them in the proper way to get the list of tools and invoke them when needed. As such, you can just copy and paste the following OpenAPI definition in the Swagger editor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
swagger: '2.0'
info:
  title: MCP Vacation Requests
  description: MCP Test Specification, showing the MVP spec to work in Copilot Studio
  version: 1.0.0
host: <your-dev-tunnel>
basePath: /
schemes:
  - https
definitions:
  QueryResponse:
    type: object
    properties:
      jsonrpc:
        type: string
      id:
        type: string
      method:
        type: string
      params:
        type: object
      result:
        type: object
      error:
        type: object
paths:
  /sse:
    get:
      summary: MCP Server Actions
      parameters:
        - in: query
          name: sessionId
          type: string
          required: false
      produces:
        - application/json
      responses:
        '200':
          description: Immediate Response
          schema:
            $ref: '#/definitions/QueryResponse'
        '201':
          description: Created and will follow callback
      operationId: InvokeMCP
      tags:
        - Agentic
        - McpSse
securityDefinitions: {}
security: []

The only thing you must change is the <your-dev-tunnel> placeholder, which should be replaced with the public URL you got from Visual Studio Code. For example, if your public URL is https://qfpn28w9-5248.euw.devtunnels.ms, you should replace the placeholder with qfpn28w9-5248.euw.devtunnels.ms (make sure to remove the HTTPS prefix). The key feature of this OpenAPI definition are the Agentic and McpSse tags, which are used by Copilot Studio to identify the custom connector as an MCP server.

Now you can click on Create connector to complete the operation and return to the custom connector page. You should see the new connector in the list. Now it’s time to create our agent!

Use the MCP server with an agent

Now let’s go back to the Copilot Studio portal and create a new agent. For our scenario, we’re going to create a custom engine agent, which are agents which enables developers to customize all their building blocks and that can be published in multiple channels. At the time of writing this post, declarative agents (which are agents that are based on the Microsoft 365 Copilot stack and that can be used only through Copilot Chat) don’t support MCP yet.

Click on the Create button in the left panel and select New agent. By default, this will trigger the experience to create a new agent through an assisted wizard based on natural language. For our demo purposes, let’s just click on Skip to configure at the top right of the page.

Now let’s provide the following information:

  • Name: Vacation Tracker Agent
  • Description: An agent that can hel p employees to track their vacation requests
  • Instructions: You are an agent which is specialized in helping employees to track their vacation requests. You can help them to request new vacation days, check the status of their requests and get the number of vacation days left.

Now hit Create at the top right of the page and wait until the agent is created. Once the operation is completed, first we need to turn on Orchestration, which enables Copilot Studio to use Generative AI to automatically determine the best course of action based on the prompt of the user. This way, if we ask the agent a question about vacation requests, it will be able to determine automatically that it needs to call the MCP server. However, now we need to actually empower the agent to use the MCP server. We do this by going to the Actions section and clicking on the Add action button.

Adding an action to an agent

To easily find the MCP server from the list, click on the three dots and choose Custom connector. This way, the gallery will be filtered to display only the custom connectors. You should see the MCP Vacation Requests connector we have previously created.

The custom connector for our MCP server

Click on it. As the first step, you will be asked to setup a connection. You won’t have to do anything, just wait until a green check shows up next to the MCP Vacation Requests connector and click Next.

Establishing a connection with the custom connector

The next step is to setup the action, by providing a description, inputs and outputs. Wait a minute, isn’t the goal of MCP to move this type of configuration from the client to the server? You’re exactly right! We have already described the purpose of our tools and the input parameters when we implemented the MCP server, so we won’t have to do anything here. We can leave the default descriptions, even if at a glance they don’t make much sense since they’re very generic.

The configuration of the action for the MCP server

Just click on Add action and wait for the operation to complete. That’s it! All the plumbing between our agent and the features offered by our vacation tracking service will be managed by the MCP protocol. However, before testing the agent, we need to make a change in the MCP server configuration to support a Copilot Studio requirement.

Customizing the MCP server for Copilot Studio

As we have seen when we launched the MCP server, by default the main endpoint returns the following message:

1
2
event: endpoint
data: message?sessionId=-JFYOh9dO5GvqMxhhTXfMg

However, Copilot Studio requires that the data property doesn’t contain just the relative path to the endpoint, but also the full URL. As such, we need to customize the MCP server implementation we built to accommodate this requirement. Unfortunately, the MCP SDK for C# doesn’t provide a way to do this out of the box, so we need to manually implement a custom MCP endpoint. Copy and paste the following code in the Program.cs file, after app.Run():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
static void MapAbsoluteEndpointUriMcp(IEndpointRouteBuilder endpoints)
{
    var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
    var options = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>().Value;
    var routeGroup = endpoints.MapGroup("");
    SseResponseStreamTransport? session = null;

    routeGroup.MapGet("/sse", async context =>
    {
        context.Response.Headers.ContentType = "text/event-stream";

        var host = $"https://qfpn28w9-5248.euw.devtunnels.ms";
        var transport = new SseResponseStreamTransport(context.Response.Body, $"{host}/message");
        session = transport;
        try
        {
            await using (transport)
            {
                var transportTask = transport.RunAsync(context.RequestAborted);
                await using var server = McpServerFactory.Create(transport, options, loggerFactory, endpoints.ServiceProvider);

                try
                {
                    await server.RunAsync(context.RequestAborted);
                }
                catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
                {
                    // Normal SSE disconnect.
                }
                catch (Exception ex)
                {
                    // Handle other exceptions as needed.
                    Log.Error(ex, "Error in SSE transport: {Message}", ex.Message);
                }

                await transportTask;
            }
        }
        catch (Exception ex)
        {

        }
    });

    routeGroup.MapPost("/message", async context =>
    {
        if (session is null)
        {
            await Results.BadRequest("Session not started.").ExecuteAsync(context);
            return;
        }

        var message = await context.Request.ReadFromJsonAsync<IJsonRpcMessage>(
            McpJsonUtilities.DefaultOptions, context.RequestAborted);
        if (message is null)
        {
            await Results.BadRequest("No message in request body.").ExecuteAsync(context);
            return;
        }

        await session.OnMessageReceivedAsync(message, context.RequestAborted);
        context.Response.StatusCode = StatusCodes.Status202Accepted;
        await context.Response.WriteAsync("Accepted");
    });
}

This code is a custom implementation of the MCP server that uses the SSE transport protocol. The key part is the host variable, which contains the public URL of the server. You must replace the URL in the sample with the dev tunnel that you have previously created in Visual Studio Code.

The final step is to remove the app.MapMcp() line we added before and replace it with the following one:

1
MapAbsoluteEndpointUriMcp(app);

This is required because we’re providing our own implementation of the MCP server, so if we would keep both we would get an error because we are trying to register two different implementations of the same endpoints.

If you now launch the server again and open the URL http://localhost:5248/sse, you should see a message like the following one:

1
2
event: endpoint
data: https://qfpn28w9-5248.euw.devtunnels.ms/message

This means that the server is now exposing the full URL of the endpoint, which is what Copilot Studio requires.

Now we can go back to the Copilot Studio portal and test our agent.

Testing the agent

Copilot Studio provides a built-in chat interface that you can use to test your agent. You will find it in a panel on the right side of the page. Type the following prompt:

1
Give me a list of all the employees and their vacation days left

The first time you execute a prompt like this, Copilot Studio will ask permissions to run this action, since it’s the first time we’re using the custom connector.

Copilot Studio asks for permission to use the custom connector

Click on Connect and you will be redirected to a page like this:

Set up a connection with the custom connector

Click on Connect under the Status column and, in the pop-up that shows up, click on Submit. Wait until the operation is completed and the status becomes Connected with a green check. Now you can go back to the chat interface and click Retry.

If everything goes well, you should see the Activity map showing the agent calling the custom connector. In the test agent panel, instead, you should see a similar response to the one that in the previous post we have observed in GitHub Copilot: a list of employees and their vacation days left.

The list of employees

Well done!

Wrapping up

In this post, we have learned how to implement an MCP server using the SSE transport protocol and how to use it with Copilot Studio. The feature is still in preview and we have seen that the experience is still not perfect:

  • We had to implement a custom MCP server to support Copilot Studio’s requirement for the full URL of endpoints, which typically isn’t required by other MCP clients.
  • We have to wrap the MCP server in a custom connector, which is a bit cumbersome and not very user friendly.

Despite that, it works really well and it’s really effective in connecting our custom engine agents built with Copilot Studio to tools exposed by the MCP server.

If you want to experiment with the sample, you can find the code in the SSE folder of the GitHub repository. Be aware that in the folder you will see other projects. The reason is that, to simplify the deployment of the solution, I’ve wrapped it in an Aspire project, which tremendously help with logging, tracing and management. We will cover more about the Aspire implementation in the next posts, however for the moment you can just ignore it and simply launch the MCP.SSE.Server project directly.

Happy coding!

Built with Hugo
Theme Stack designed by Jimmy