Blog Post

Apps on Azure Blog
11 MIN READ

Develop Custom Engine Agent to Microsoft 365 Copilot Chat with pro-code

daisami's avatar
daisami
Icon for Microsoft rankMicrosoft
Jul 29, 2025

There are some great articles that explain how to integrate an MCP server built on Azure with a declarative agent created using Microsoft Copilot Studio. These approaches aim to extend the agent’s capabilities by supplying it with tools, rather than defining a fixed role. Here were some of the challenges w encountered: The agent's behavior can only be tested through the Copilot Studio web interface, which isn't ideal for iterative development. You don’t have control over which LLM is used as the orchestrator—for example, there's no way to specify GPT-4o. The agent's responses don’t always behave the same as they would if you were prompting the LLM directly. These limitations got me thinking: why not build the entire agent myself? At the same time, I still wanted to take advantage of the familiar Microsoft 365 Copilot interface on the frontend. As I explored further, I discovered that the Microsoft 365 Copilot SDK makes it possible to bring in your own custom-built agent.

Getting Started

Here is the original article written in Japanese. There are some great articles that explain how to integrate an MCP server built on Azure with a declarative agent created using Microsoft Copilot Studio. These approaches aim to extend the agent’s capabilities rather than defining a role. Here were some of the challenges we encountered:

  • The agent's behavior can only be tested through the Copilot Studio. We want to use pro-code development tools.
  • We can't choose LLM as the orchestrator—for example, there's no way to specify GPT-4o.
  • The agent doesn’t always response same one which they would if we were prompting the LLM directly.

These limitations got me thinking: why not build the entire agent myself? At the same time, I still wanted to take advantage of the familiar Microsoft 365 Copilot interface on the frontend. As I explored further, I discovered that the Microsoft 365 Copilot SDK makes it possible to bring in your own custom-built agent. In short, this approach aligns perfectly with what we want to do:

References

From Building a Custom Agent to Publishing It to the Copilot Agent Store

The basic process follows the reference articles, but here’s a high-level overview of the steps involved:

  1. Set up your development environment
  2. Customize the project template
  3. Debug the agent locally
  4. Create an Azure Bot Service and connect it
  5. Publish the agent to the Microsoft 365 Copilot Agent Store

The goal is to publish the agent to the Copilot Agent Store—but here's the catch: what we will actually develop is a Teams app.

1. Set Up the Development Environment

According to the M365 Copilot SDK documentation, it supports C#, JavaScript, and Python. We will be going with our beloved C#. The recommended approach for development is to use the Agent Toolkit, so we will install it. There’s no need to download anything manually; instead, you can simply add the required components via the Visual Studio Installer as follows:

2. Customize the project template

When creating a new project, search for "agent" or similar keywords. As shown in the screenshot below, you’ll find a Teams app template—this is the one to use.

There are several templates to choose from, but we will use the one labeled Custom Engine Agent - in this case, we’ll select the Weather Agent template. During the setup process, we will configure our Azure OpenAI endpoint. We can either create a new one or reuse an existing one. For now, we will simply use GPT-4o as LLM.

Once the project is created, we will get a solution with two projects as follows:

  • M365Agent project: This contains metadata required for the Teams app, as well as configuration for the local debug emulator.
  • The project you created: This is essentially a standard ASP.NET Core Web API, but it's already pre-configured to connect with Azure Bot Service.

Next, let’s take a quick look at the source codes.

NuGet Packages in Use

First, looking at the referenced packages, we can see that it includes several with names like Microsoft.Agents.*, which appear to be part of the M365 Agent SDK, as well as packages related to Semantic Kernel, such as Microsoft.SemanticKernel.

Here are some of the key packages included:

  • Azure.Identity
  • AdaptiveCards
  • Microsoft.SemanticKernel.Agents.AzureAI
  • Microsoft.SemanticKernel.Agents.Core
  • Microsoft.SemanticKernel.Connectors.AzureOpenAI
  • Microsoft.SemanticKernel.Connectors.OpenAI
  • Microsoft.Agents.Authentication.Msal
  • Microsoft.Agents.Hosting.AspNetCore

The documentation mentions that conversations are handled by an LLM and the control flow is managed using Semantic Kernel.

Startup Code

Next, let’s take a look at the startup code in Program.cs. Since the file is long, we highlight only the key parts here:

// Standard ASP.NET Core app setup
var builder = WebApplication.CreateBuilder(args);

// Register Semantic Kernel
builder.Services.AddKernel();

// Register Azure OpenAI parameters (from appsettings.{env}.json)
// These were provided during project creation
builder.Services.AddAzureOpenAIChatCompletion(
    deploymentName: config.Azure.OpenAIDeploymentName,
    endpoint: config.Azure.OpenAIEndpoint,
    apiKey: config.Azure.OpenAIApiKey
);

// Register the actual implementation of the AI agent
builder.Services.AddTransient<WeatherForecastAgent>();

// Set up authentication for inbound requests from Azure Bot Service
builder.Services.AddBotAspNetAuthentication(builder.Configuration);

// Register components for handling communication with Azure Bot Service
builder.AddAgentApplicationOptions();
builder.Services.AddTransient<AgentApplicationOptions>();
builder.AddAgent<MyM365Agent0702.Bot.WeatherAgentBot>();

var app = builder.Build();

// Route for receiving messages from Azure Bot Service
// Uses IAgentHttpAdapter to relay the request to the IAgent implementation
app.MapPost(
    "/api/messages",
    async (HttpRequest request, HttpResponse response, IAgentHttpAdapter adapter, IAgent agent, CancellationToken cancellationToken) =>
    {
        await adapter.ProcessAsync(request, response, agent, cancellationToken);
    }
);

app.Run();

There’s only a single API route configured, and all incoming requests are relayed to the IAgent. In this setup, the IAgent implementation is WeatherAgentBot, which was registered using AddAgent(). From here, we just need to the configuration files accordingly.

Implementation of the IAgent Interface

Now, let’s take a look at the WeatherAgentBot. We highlight only key parts here to give you a high-level overview. For full details, please refer to the actual generated code.

public class WeatherAgentBot : AgentApplication
{
    public WeatherAgentBot(AgentApplicationOptions options) : base(options)
    {
        // Register event handlers
        OnConversationUpdate(ConversationUpdateEvents.MembersAdded, WelcomeMessageAsync);
        OnActivity(ActivityTypes.Message, MessageActivityAsync, rank: RouteRank.Last);
    }

    // Handles when a new member joins the conversation
    protected async Task WelcomeMessageAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
    {
        await turnContext.SendActivityAsync(
            MessageFactory.Text("Hello and Welcome! I'm here to help with all your weather forecast needs!"),
            cancellationToken);
    }

    // Handles incoming messages from the user
    protected async Task MessageActivityAsync(ITurnContext turnContext, ITurnState turnState, CancellationToken cancellationToken)
    {
        // Notify Azure Bot Service that a response is in progress
        await turnContext.StreamingResponse.QueueInformativeUpdateAsync("Working on a response for you");

        // Instantiate the actual agent logic
        _weatherAgent = new WeatherForecastAgent(_kernel, serviceCollection.BuildServiceProvider());

        WeatherForecastAgentResponse forecastResponse = await _weatherAgent.InvokeAgentAsync(turnContext.Activity.Text, chatHistory);

        // Send response back via Azure Bot Service
        switch (forecastResponse.ContentType)
        {
            case WeatherForecastAgentResponseContentType.Text:
                turnContext.StreamingResponse.QueueTextChunk(forecastResponse.Content);
                break;

            case WeatherForecastAgentResponseContentType.AdaptiveCard:
                turnContext.StreamingResponse.FinalMessage = MessageFactory.Attachment(new Attachment
                {
                    ContentType = "application/vnd.microsoft.card.adaptive",
                    Content = forecastResponse.Content
                });
                break;

            default:
                break;
        }

        // Signal end of streaming
        await turnContext.StreamingResponse.EndStreamAsync(cancellationToken);
    }
}

It looks like here:

  • During initialization, the agent registers handlers for corresponding events using the OnXXXX methods.
  • The agent can send replies by processing the turnContext.
  • The agent can reply asynchronously by placing items into Queue for the StreamingResponse (The agent won't block as synchronous processes).

Implementation of the Agent

This section covers the main body of the agent.

// A very standard class
public class WeatherForecastAgent
{
    // ChatCompletionAgent from Semantic Kernel
    private readonly ChatCompletionAgent _agent;

    // Kernel and service (assumed based on usage)
    private readonly Kernel _kernel;
    private readonly IServiceProvider _service;

    // Agent name and prompt
    private const string AgentName = "WeatherForecastAgent";
    private const string AgentInstructions = """
        You are a friendly assistant that helps people find a weather forecast for a given time and place.
        ...
        """;

    // Constructor used for initialization from the previously mentioned WeatherAgentBot
    public WeatherForecastAgent(Kernel kernel, IServiceProvider service)
    {
        _kernel = kernel;
        _service = service;

        // Create a ChatCompletionAgent from Semantic Kernel
        _agent = new ChatCompletionAgent
        {
            // ... your agent configuration here ...
        };
    }

    // Method called from the previously mentioned WeatherAgentBot
    public async Task<WeatherForecastAgentResponse> InvokeAgentAsync(string input, ChatHistory chatHistory)
    {
        WeatherForecastAgentResponse result = new();

        // Restore the chat history and invoke the ChatCompletionAgent from Semantic Kernel
        await foreach (ChatMessageContent response in _agent.InvokeAsync(chatHistory, thread: input))
        {
            // ... process the response ...
        }

        // Return the result
        return result;
    }
}

It just passes through to the Semantic Kernel’s ChatCompletionAgent. Thus, this part isn't the core logic and can be freely customized. It would even be reasonable to implement this directly inside the WeatherAgentBot, but keeping the implementation separated like this tends to be more convenient in various ways. If we might have existing AI agent code, we could reuse the code by implementing the IAgent interface.

3. Debug the Agent Locally

Now it's about time to test it. Since we're using Visual Studio, we can simply press F5 to start debugging, but let's quickly go over the configuration.

This solution is set up as a multi-startup project. While debugging the ASP.NET Core part we just walked through in the source code, it also launches the Agent Playground from the other project. Let’s check the selected startup configuration and begin debugging.

Once you confirm that the Playground has started, you can try interacting with it. You’ll see logs appear in the standard output of the ASP.NET Core project that’s running in debug mode. The log shown here is a trimmed version, but we can get a good sense of what’s going on. It shows the use of plugins added to Semantic Kernel to retrieve the current date, fetch the weather forecast, and then summarize the results using the LLM.

4. Create an Azure Bot Service and connect it

To publish the agent we created to the Agent Store, we need to create an Azure Bot Service and connect it to the locally running agent. We’ll build a setup like the one shown below.

Create the Azure Bot Service

There aren’t many options to configure when creating an Azure Bot Service, but make sure to select the following settings. The Entra ID-registered application configured here will represent the Bot Service, your .NET application, and the Teams app.

OptionSettingNotes
Type of Appsingle-tenantDefines the scope of Entra ID authentication
Creation TypeCreate a new Microsoft App IDRegisters a new app in Entra ID

Retrieve Service Principal and Update Bot Service Configuration

When the Azure Bot Service is created, an application will also be registered in Entra ID. This application provides the authentication credentials used for communication between services, so make sure to store the information securely and avoid any leaks.

  • In the Configuration menu of the Azure Bot Service, you’ll see both the Microsoft App ID and App Tenant ID. Make sure to copy and save these values.
  • Click the Manage Password link to create a Client Secret. Generate a new client secret and securely save the value.
  • The Messaging Endpoint will be the URL of the tunnel you’ll create later, so you can leave it as-is for now.

Next, enter the saved values into appsettings.Development.json.

{
  "TokenValidation": {
    // The GUID value is the Application ID registered in Entra
    "Audiences": [
      "your-application-id-guid"
    ]
  },
  "Connections": {
    "BotServiceConnection": {
      "Settings": {
        // Authentication is done using a client secret
        "AuthType": "ClientSecret",
        // The GUID value is the Tenant ID in Entra
        "AuthorityEndpoint": "https://login.microsoftonline.com/your-tenant-id-guid",
        // The GUID value is the Application ID registered in Entra
        "ClientId": "your-application-id-guid",
        // The value of the created client secret
        "ClientSecret": "your-application-client-secret",
        "Scopes": [
          "https://api.botframework.com/.default"
        ]
      }
    }
  },
  // Azure OpenAI service connection information set when creating the project
  "Azure": {
    "OpenAIApiKey": "your-api-key",
    "OpenAIEndpoint": "https://resourceName.openai.azure.com",
    "OpenAIDeploymentName": "gpt-4o"
  }
}

From a security perspective, these settings should ideally be stored in user secrets, but we take a shortcut because here is development environment. Please make sure not to push them as-is to your source code repository.

Run the agent in debug mode and connecting via Dev Tunnel

Azure Bot Service needs to send messages to the agent, which is an ASP.NET Core app here. But it cannot reach endpoints hosted only on your local development machine. Hosting the endpoint on a public service like App Service makes debugging and verification harder, so let's create a development tunnel instead.

First, switch the debug target using the toolbar on Visual Studio. The profile named Start Project is defined in your project's Properties/launchSettings.json. In this file, the environment variable ASPNETCORE_ENVIRONMENT is set to Development, so when you launch with this profile, the settings from appsettings.Development.json will be used. If you accidentally use the Playground profile, it will run with different settings, and the app won’t work properly. This could cause you to be stuck for a while....

Next, create a development tunnel to allow external access to the project you will run in debug mode.

  • You can use any name as you like.
  • If you set the tunnel type to persistent, the URL will remain fixed, which makes the subsequent steps easier.
  • If you set the access to public, Dev Tunnel won’t require authentication to access it, making the following tasks more convenient.

It's finally time to debug the agent. You should see the console start up, with the ASP.NET Core web server running at localhost:5130, and you can confirm that the hosting environment is set to Development.

Open the Development Tunnel window in Visual Studio and click the output log icon. You’ll see the URL where the development tunnel is listening and the URL looks like this:

  • https://unique-id.asse.devtunnels.ms

Let's access this URL, then you’ll see “Weather Bot” displayed. This corresponds to the route registered in Program.cs that’s enabled when running in the Development environment.

Testing the agent from Azure Bot Service

Now, append /api/messages to the Dev Tunnel URL you obtained earlier and configure it as the messaging endpoint for Azure Bot Service. You can then test the functionality using the test menu in Web Chat. When you start the chat, the locally running ASP.NET Core app will be invoked. It’s a bit hard to explain in words, so here’s a video demonstration.

The temperature is getting a bit crazy, but it works and improving the agent’s quality isn’t the main point here.

5. Publish the agent to the Microsoft 365 Copilot Agent Store

Next, we will publish the agent as a Teams app, so it’s not too complicated.

Configuring the Teams Channel in Azure Bot Service

First, connect your Bot Service to Microsoft Teams.

  • Open the Channels menu and select Microsoft Teams from the available channels.
  • Agree to the terms of service and click Apply without changing any options.
  • Microsoft Teams will now appear as one of the connected channels.

Creating and Uploading the Teams App Manifest

Next, we register the Teams app, but we have to create its registration information (manifest) before that. In the Visual Studio project for M365Agent, we can find a folder named appPackage that contains a manifest.json along with image files. Within the manifest.json, there are several ID fields that we have to update—specifically, replace all of them with the clientid of the Entra ID application.

The fields to update include:

  • id
  • copilotAgents.customEngineAgents.id
  • bots.botId

It’s also helpful to update other values such as the display name, description, and version to make this app easier to identify. Compress the three files inside the appPackage folder into a single .zip file.

Now open the Microsoft Teams Admin Center and upload the zipped app manifest. It may take a few moments to process, but once completed, you’ll be able to view the detailed app information as shown below.

Trying it out from Teams

It may take a little time for the changes to propagate, but once your agent appears in the Agent Store—like in the initial screenshot— we have successfully published the agent.

Once you’ve reached this point, the rest is standard development flow: iteratively improving your agent, testing it via Copilot or Teams, and debugging its behavior locally. Since the developed agent is just a regular ASP.NET Core app, you can deploy it to any environment. Just make sure the messaging endpoint is accessible from Azure Bot Service, meaning it needs to have a public endpoint. Hosting it on a PaaS service like Azure App Service or Azure Container Apps is a good choice. After deployment, don’t forget to update the endpoint URL accordingly.

Conclusion

In this article, we used the default project template, but we've confirmed that it's possible to deliver a custom-developed agent to users as a Microsoft 365 Copilot-compatible agent. The key part of the architecture is arguably the Azure Bot Service, which acts as an intermediary.

As shown in the steps above, the agent itself does not live in the Teams or M365 Copilot environment. Instead, it resides behind the messaging endpoint specified in Azure Bot Service. This means that if we want to update or replace the agent, all we need to do is switch what’s behind that endpoint.

The agent should be continuously improved through a DevOps-like cycle—collecting telemetry, tuning the LLM, instructions, knowledge, and tools. These days, this process might even be referred to as “AgentOps.” The beauty of this setup is that no changes are required on the Teams side, allowing for

seamless updates with no disruption to end users.

Here is next step for this Custom Engine Agent to Microsoft 365 Copilot Chat development. Please refer to following article to improve your custom engine agent security.

Updated Aug 15, 2025
Version 8.0

1 Comment

  • Love the idea of building a full agent while still leveraging the M365 Copilot UI. Great articulation throughout. 

    I especially appreciate how it combines pro-code Azure AI, C#, VS, Functions etc. with a low-code UX.

    Thanks for sharing, Daichi!