Skip to content
ArticlesCopilot Studio

Copilot Studio

Build Your Own MCP Server and Wire It Into Copilot Studio

A hands-on walkthrough for building a custom Model Context Protocol server in Python, exposing it publicly, and connecting it to a Copilot Studio agent - including the wizard gotchas, dynamic tool discovery, and the DLP governance angle nobody talks about.

Most Copilot Studio tutorials stop at prebuilt connectors and knowledge sources. That is fine for a demo, but it hides the one architectural move that separates a maker from an architect: giving an agent tools that you own, backed by your own logic, running on your own server.

The Model Context Protocol (MCP) is the open standard that lets an agent discover and call tools over HTTP. Copilot Studio speaks it natively. This article walks through building a small MCP server in Python, exposing it to the internet, wiring it into an agent through the maker portal, and proving two things that matter in production: tools resolve in under a second, and new tools appear without touching the agent config. Along the way it flags the wizard behaviors that will cost you twenty minutes each if nobody warns you first.

Why Custom MCP Is the Architect's Brick

Prebuilt connectors answer "what can this agent reach." A custom MCP server answers "what can this agent do that only exists in your world." The status of an internal project, the owner of a record in a system with no Power Platform connector, a capacity calculation that lives in a spreadsheet nobody wants to migrate - all of it becomes a tool the moment you expose it over MCP.

The second reason is governance, and it is the part that gets overlooked. When you register an MCP server in Copilot Studio, it does not stay a loose HTTP endpoint. It materializes as a custom connector in your environment. That single fact means every guardrail your tenant already applies to connectors - Data Loss Prevention classification, environment scoping, sharing controls - now applies to your agent's custom tools without any extra work. More on that below.

The Server in About Thirty Lines

The reference implementation for a Python MCP server is FastMCP. It handles the protocol handshake, tool registration, and the Streamable HTTP transport so you write plain functions. Here is the shape of a server that exposes three tools over fictional project data:

Code
from fastmcp import FastMCP

mcp = FastMCP("project_hub")

PROJECTS = {
    "PRJ-002": {"name": "Helios Reporting", "status": "At Risk", "owner": None},
}

@mcp.tool
def get_project_status(project_id: str) -> dict:
    """Return status and owner for a project by id."""
    return PROJECTS.get(project_id, {"error": "not found"})

@mcp.tool
def assign_owner(project_id: str, owner: str) -> dict:
    """Assign an owner to a project."""
    PROJECTS[project_id]["owner"] = owner
    return PROJECTS[project_id]

@mcp.tool
def list_open_risks() -> list:
    """List all open risks across projects."""
    return [{"id": "RSK-11", "project": "PRJ-002", "severity": "High"}]

if __name__ == "__main__":
    mcp.run(transport="http", host="127.0.0.1", port=8787)

Three decorators, three tools. The docstring is not decoration: it becomes the tool description the agent's planner reads when it decides whether to call your function. Write it like a prompt, because it is one.

One trap worth stating now, because it will bite during the dynamic-discovery demo later: every @mcp.tool must be defined before the mcp.run() call. A tool function appended to the bottom of the file, after run(), is never registered. The server starts, looks healthy, and silently serves the old tool list.

Exposing It Without Infrastructure

The agent's backend runs in Microsoft's cloud, so it needs a public URL to reach your server. For a proof of concept you do not need a VM or a reverse proxy. A quick tunnel does it:

Code
cloudflared tunnel --url http://127.0.0.1:8787

This prints a public HTTPS URL of the form https://your-tunnel.trycloudflare.com. Your MCP endpoint is that host plus /mcp. Two things to know: the URL regenerates on every run, so grab it fresh from the console each time, and the tunnel is ephemeral - fine for a POC, not for anything you leave running.

Before touching Copilot Studio, prove the server answers. A tiny MCP client that runs initialize then tools/list against the public /mcp URL should return the handshake plus your three tools. If that works over the tunnel, the wizard will work too. If it does not, no amount of clicking in the portal will save you.

The Wizard, Step by Step, With the Gotchas

In the maker portal, open your agent and go to Tools > Add a tool > New tool > Model Context Protocol. The wizard asks for a server name, a description, the server URL, and an authentication mode: None, API key, or OAuth 2.0. For a no-auth POC, fill in the tunnel URL with /mcp, choose None, and create.

That is the happy path. Here is what the happy path does not tell you.

The second surprise: even with authentication set to None, Copilot Studio still requires a connection before the agent can call the tool. Creating it is two clicks, but the first time you test, the agent may reply with an adaptive card that says something like "let us connect first." When that happens, open the connection manager from that card, confirm the connection, and hit Retry. It is not an error - it is the platform wiring the connector to your session.

A third, quieter one: the maker portal is a heavy single-page app. Deep links straight to /tools can freeze the renderer. Navigate from the agent home page instead of pasting a tools URL, and give the agent a twenty to twenty-five second warm-up on its first call while the backend cold-starts the connector.

The Demo: Real Data, Then Dynamic Discovery

With the tool wired in, ask the agent a question that only your server can answer: "What is the status of project PRJ-002?" The agent builds an execution plan, calls get_project_status, and returns the answer - "Helios Reporting, At Risk, unassigned" - straight from the FastMCP process running on your laptop, through the tunnel. In testing this call resolved in about 0.80 seconds, and the server log showed the POST requests arriving from Microsoft's backend IP range. That round trip is the whole point: the agent reasoned, picked your tool, and executed it.

Now the part that makes MCP worth the setup. Add a fourth tool to the server - get_team_capacity, say - restart the server, and change nothing in the agent. This is where the documentation's promise ("Copilot Studio dynamically reflects changes to your MCP server") meets a nuance that matters.

That distinction is easy to miss and embarrassing to get wrong in front of an audience. The tool set is refreshed per session, not per turn. Plan your demo around a new chat, not a follow-up message.

The Governance Angle: DLP for Free

Here is the payoff for anyone who has to answer to a platform admin. Because your MCP server registered as a custom connector, it shows up in the Power Platform Admin Center under Policies > Data policies like any other connector. An admin can classify it as Business, Non-Business, or Blocked, and place it in a DLP group alongside SharePoint, Dataverse, and the rest.

This is the difference between shadow IT and a governable capability. The same server that hands your agent a bespoke tool also hands your admin a row in a DLP policy. You do not have to choose between capability and control.

From POC to Production: What Actually Changes

The POC above is honest about being a POC. Three things change before this is something you would run for real users:

  • Host. The trycloudflare.com tunnel is ephemeral and regenerates its URL on every run. Production needs a stable host - a container app, a named tunnel, or an App Service - with a URL that does not move, so the connector configuration stays valid.
  • Authentication. "None" is fine for fictional demo data. Real tools need OAuth 2.0, which Copilot Studio supports through dynamic client registration. Nobody should be able to call your capacity or assignment tools anonymously over the open internet.
  • Allowlisting and DLP. Put the connector in the right DLP group deliberately, restrict which environments can use it, and treat tool changes as changes to a governed asset - because that is what they are.

The server code barely changes between POC and production. What changes is everything around it: where it runs, who can call it, and how it is governed. Get the thirty lines working first, prove the handshake, run the dynamic-discovery demo, then harden.

The reason to learn this now is that "add a knowledge source" is a skill anyone can pick up in an afternoon, and "build the tool the agent calls" is the one that makes you the person who designs the system rather than the person who configures it.