Building a C# MCP Server for Microsoft 365 Copilot

Michael HoferMichael Hofer
5 min read

I recently wrestled with the following challenge: how to quickly spin up a standalone Model-Context-Protocol (MCP) server for an existing API in .NET C# and then integrate it seamlessly with a Custom Agent in Microsoft 365 Co-Pilot. Why "standalone," you ask? Well, there are plenty of tools out there that claim to turn an existing web API into an MCP server with a snap of your fingers. While tempting, that's a rabbit hole for another day and another blog post!

My mission, as always, was to deliver a usable Proof-of-Concept (PoC) ASAP. This post is all about sharing the end-to-end journey and the tools that got me there, because, let me tell you, getting a custom MCP server into Microsoft 365 Copilot isn't exactly a walk in the park. At least, not yet.

From Azure Functions to Container Apps: The Hosting Saga

Our existing Web API was happily living in Azure, so naturally, the MCP server needed to join the party there too. After a quick fling with Azure Functions (they always look so promising at the start, don't they, only to introduce a cascade of problems that negate all their initial charm?), I swiftly pivoted to Azure Container Apps. Much smoother sailing!

Tip 1: Your starting point for an MCP Server in C# on Azure Container Apps: https://github.com/microsoft/mcp-dotnet-samples

I dove straight into the "markdown-to-html" implementation and ignored the other projects. What's awesome about this setup is that the core functionality resides in a Common project, which is then consumed by different clients (like a STDIO Command-Line App and an SSE endpoint in the Container App). This makes adaptation incredibly fast, and with the Azure Developer CLI (azd up), deployment to the cloud is a breeze.

The Microsoft 365 Copilot Integration Headaches

Now, for the fun part: integrating with Microsoft 365 Copilot. This is where things got a bit… spicy. While there are various guides out there, they often feel incomplete or just don't quite hit the mark (or maybe I'm just slow on the uptake!).

As of today, Microsoft 365 Copilot doesn't really offer robust, native MCP Server connectors. You have to go through a Custom Connector, which has its roots in the Power Platform. This, in turn, needs to be enabled for users/agents via a Connection. The sheer infrastructure dependencies here can (and did) throw a wrench into the works, especially in a somewhat restricted environment.

But let's assume you've got your Custom Connector and Connection squared away. The million-dollar question remains: How on earth do you configure the Custom Connector to actually recognize an MCP server and its tools?

Tip 2: My savior for an MCP Server with an SSE Endpoint: https://www.developerscantina.com/p/mcp-copilot-studio/ by Matteo Pagani, a Cloud Solution Architect at Microsoft.

Matteo's blog post was an absolute lifesaver. He meticulously walks through the Custom Connector configuration, revealing crucial insights. We're talking gold nuggets like the fix for Copilot's insistence on absolute URLs in MCP server responses, and the specific Swagger definition tags needed for Copilot to identify an MCP interface. Phew! What a breakthrough that was!

I took his code and enhanced it further to dynamically pull the URL from the request (no more static entries!) and added an environment variable to toggle the Microsoft 365 Copilot fix. Let me know, if you are interested in this.

And guess what? Our custom MCP server is now happily chugging along with Microsoft 365 Copilot. Not too shabby at all!

A quick heads-up: For now, explicitly switch your model to (currently) ChatGPT 4.1. ChatGPT 4o isn't reliably calling the tools and is, frankly, a bit "dumb" when it comes to parameters. Well, it is over a year old now, so… 😉

Embracing the Future: HTTP Stream Transport

Just when I thought I was done, SSE is already becoming old news! The new kid on the block is HTTP Stream Transport. Naturally, I had to keep up. Thankfully, Matteo kindly pointed out that Microsoft 365 Copilot now supports streaming HTTP, making the whole workaround I just described (and built!) obsolete. Ah, the joys of fast-paced tech!

Tip 3: For an MCP Server with Streaming HTTP Stream support: https://www.developerscantina.com/p/mcp-copilot-studio-streamable-http/ – again, from the legendary Matteo Pagani!

The best part? You can essentially ditch the entire fix from the first example and simply register the MCP server via "Import from GitHub." How neat is that? I've seen this example used for a Chuck Norris Jokes MCP Server implemented in TypeScript, and I initially thought, "Huh? This must be something super specialized." But nope, this is the way to go. At least for now. Because, let's be real, the "fix" for SSE was probably only going to last a few days or weeks anyway! But hey, it works (for now)!

What's Next: Authentication and Authorization

I hope these tips help you integrate your own custom MCP server into Microsoft 365 Copilot! If not, feel free to reach out.

Finally, a Tip 4, which I haven't had a chance to verify or try out myself yet. So far, we've focused on a moderately unsecured and unpersonalized MCP server. What we ultimately want is a secure, ideally user-context-aware server that can also interact with OAuth-secured APIs.

Tip 4: Authentication & Authorization for MCP Servers: https://github.com/nucleo-tidz/dev-model-context-protocol by Ahmar Husain.

This project is also in .NET C# and all MCP servers are secured using Microsoft Entra ID:

  • Authentication: Agents authenticate using OAuth2 client credentials

  • Authorization: Role-based access control (RBAC) ensures agents only access permitted scopes

Depending on how my current project progresses, I'll dive deeper into these security aspects and potentially follow up with another blog post.

What challenges have you faced when integrating custom solutions with Microsoft 365 Copilot? Share your experiences in the comments below!

0
Subscribe to my newsletter

Read articles from Michael Hofer directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Michael Hofer
Michael Hofer