Tutorials/Copilot Studio/Build a Custom MCP Server for Your Copilot Studio Agents
Copilot Studiointermediate

Build a Custom MCP Server for Your Copilot Studio Agents

Learn how to create a Python-based MCP server, test it with MCP Inspector, deploy it as an Azure container app, and connect it to Copilot Studio.

NA
Narmer Abader
@narmer · Published June 3, 2026

The Model Context Protocol (MCP) is rapidly becoming the standard for connecting AI agents to live data and services. With a custom MCP server, you can give your Copilot Studio agent the ability to perform real-time lookups or execute actions that go beyond static knowledge. In this tutorial, we build a currency conversion MCP server using Python, FastMCP, and the free Frankfurter exchange rate API, then deploy it as an Azure Container App and connect it to a Copilot Studio agent.

Prerequisites

To follow along, you need:

  • Copilot Studio access (licensed user)
  • Python 3.12 or later installed (3.13 recommended; matches the Dockerfile base image)
  • uv package manager (install via curl -LsSf https://astral.sh/uv/install.sh | sh on macOS/Linux, or powershell -c "irm https://astral.sh/uv/install.ps1 | iex" on Windows; see astral.sh/uv)
  • Docker Desktop (for local container builds)
  • VS Code or any code editor
  • Git
  • GitHub account
  • Azure CLI installed and logged in
  • An active Azure subscription

Project setup

Create a new directory and initialise a Python project with uv:

mkdir forex-mcp-server
cd forex-mcp-server
uv init

Edit the generated pyproject.toml to add the necessary dependencies:

tomlpyproject.toml
[project]
name = "forex-mcp-server"
version = "0.1.0"
description = "MCP server that exposes currency exchange rate tools"
requires-python = ">=3.12"
dependencies = [
  "fastmcp>=1.0.0",
  "httpx",
  "uvicorn[standard]",
]

Install the packages:

uv lock
uv sync
Virtual environment

uv sync automatically creates a .venv folder inside the project and installs all dependencies there.

Create these files in the project root:

  • main.py – the MCP server code
  • Dockerfile – container definition
  • .dockerignore – excludes unneeded files from the Docker build

Writing the MCP server code

Open main.py and write the server using the FastMCP framework.

pythonmain.py
from fastmcp import FastMCP
import httpx
from starlette.responses import JSONResponse
import uvicorn

# Create the MCP server instance
server = FastMCP("forex-exchange")

@server.tool()
async def list_currencies() -> list:
  """
  List all available currencies with their full names.

  Returns:
      A list of objects, each containing a currency code and
      its corresponding name.
  """
  async with httpx.AsyncClient() as client:
      response = await client.get("https://api.frankfurter.app/currencies")
      response.raise_for_status()
      data = response.json()
  return [{"code": code, "name": name} for code, name in data.items()]

@server.tool()
async def convert_currency(
  amount: float, from_currency: str, to_currency: str
) -> float:
  """
  Convert an amount from one currency to another using live rates.

  Args:
      amount: The numeric quantity to convert (e.g., 250.75)
      from_currency: ISO 4217 code of the source currency (e.g., "USD")
      to_currency: ISO 4217 code of the target currency (e.g., "EUR")

  Returns:
      The equivalent amount in the target currency, rounded to
      two decimal places.
  """
  async with httpx.AsyncClient() as client:
      url = (
          f"https://api.frankfurter.app/latest?from={from_currency}"
          f"&to={to_currency}"
      )
      response = await client.get(url)
      response.raise_for_status()
      data = response.json()
      rate = data["rates"].get(to_currency)
      if rate is None:
          raise ValueError(
              f"No exchange rate available for {to_currency}"
          )
      converted = amount * rate
  return round(converted, 2)

# Health check endpoint used by Azure Container Apps
@server.custom_route("/health", methods=["GET"])
async def health_check(request):
  return JSONResponse({"status": "healthy"})

# Expose ASGI app
app = server.http_app()

if __name__ == "__main__":
  uvicorn.run(app, host="0.0.0.0", port=8000)
Docstrings matter

Copilot Studio reads the Args: and Returns: sections to understand when and how to call each tool. Missing or vague descriptions will prevent the agent from using the tools correctly.

Testing locally with MCP Inspector

FastMCP includes MCP Inspector, a browser-based tool for testing your server.

  1. Start the server in a terminal:

    uv run main.py
    

    You should see Uvicorn running on http://0.0.0.0:8000.

  2. Launch the inspector in a second terminal:

    fastmcp dev main.py
    

    A page opens at http://127.0.0.1:5173 and automatically connects to your server.

  3. Use the inspector to test list_currencies – you should see a list of currency codes and names.

  4. Test convert_currency with parameters like amount=100, from_currency=USD, to_currency=EUR. The tool returns the converted value.

Containerising and deploying to Azure

Dockerfile

dockerfileDockerfile
FROM python:3.13-slim

WORKDIR /app

# Install uv and project dependencies
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen

# Copy the rest of the application
COPY . .

EXPOSE 8000

CMD ["uv", "run", "main.py"]

Add a .dockerignore:

.venv
__pycache__
.git
.gitignore

Build and push to Azure Container Registry

Login to Azure and create the required resources:

az login

az group create --name forex-mcp-rg --location eastus

az acr create --resource-group forex-mcp-rg \
  --name forexmcpacr --sku Basic --admin-enabled true

Build the Docker image and push it to the registry:

az acr build --registry forexmcpacr --image forex-mcp:latest .

Deploy to Azure Container Apps

Create a container app environment and the app itself:

az containerapp create \
  --name forex-mcp \
  --resource-group forex-mcp-rg \
  --environment forex-mcp-env \
  --image forexmcpacr.azurecr.io/forex-mcp:latest \
  --target-port 8000 \
  --ingress external \
  --registry-server forexmcpacr.azurecr.io \
  --registry-identity system \
  --query configuration.ingress.fqdn

The command outputs the fully qualified domain name (FQDN), for example forex-mcp.ashyrock-abc123.eastus.azurecontainerapps.io. Verify the health endpoint:

curl https://forex-mcp.ashyrock-abc123.eastus.azurecontainerapps.io/health

You should see {"status":"healthy"}.

HTTPS out of the box

Azure Container Apps automatically provisions an HTTPS certificate when ingress is set to external.

Connecting the MCP server to Copilot Studio

  1. Go to Copilot Studio.
  2. Create a new agent or open an existing one.
  3. Navigate to Settings (gear icon) → AI toolsMCP servers.
  4. Click Add MCP server.
  5. Enter a display name (e.g., “Forex Exchange”) and the base URL of your deployed container app (the FQDN from the previous step, without any path).
  6. Click Save. Copilot Studio attempts to connect. If successful, you’ll see the two tools (list_currencies and convert_currency).

Using the MCP tools in an agent

Create a topic that triggers on “Convert {amount} {fromCurrency} to {toCurrency}”. Inside the topic, add a Call MCP tool node. Choose the Forex Exchange connection and the convert_currency tool, then map the variables from the trigger phrase to the tool parameters. Store the result in a variable and display it in a message.

Test the agent in the chat canvas: “Convert 50 USD to GBP”. It should reply with the live exchange rate.

Security and performance considerations

  • Authentication: The example uses a public server and a public API. For workloads with sensitive data, protect your MCP server using an API key, Azure Managed Identity, or a custom authentication middleware in FastMCP.
  • Environment variables: Store any secrets (e.g., API keys as fallback) as environment variables in the container app. Use az containerapp update --set-env-vars or Key Vault references.
  • Rate limiting: The Frankfurter API may impose rate limits. Cache responses if your agent makes frequent conversions, and add graceful error handling.
  • Connection pooling: Reuse httpx.AsyncClient across tools (as shown in the code) to reduce the overhead of creating new HTTP sessions.
  • Error handling: Always wrap async HTTP calls in try/except blocks and return user-friendly error messages so the agent can handle failures gracefully.

Common mistakes

  • Missing or vague docstrings: Without a clear description of arguments and return values, Copilot Studio will either ignore the tool or call it with wrong parameters.
  • Long-running synchronous tools: FastMCP supports both sync and async tools, but CPU-intensive synchronous functions block the event loop. If your tool makes network calls or does heavy computation, declare it async so other requests are not held up.
  • No health endpoint: Azure Container Apps pings /health to determine container readiness. Without it, the container may be repeatedly restarted.
  • Wrong port: Verify the container exposes port 8000 and that --target-port 8000 is passed during deployment.
  • HTTP during local testing: Copilot Studio only connects over HTTPS. If you try a local test with http://localhost:8000, the connection will fail. Azure Container Apps handles HTTPS automatically when ingress is external.

Conclusion

Building a custom MCP server with Python and FastMCP is a straightforward way to give your Copilot Studio agents live data access and actions. Start with a simple server like the one in this walkthrough, then expand it with additional tools, proper authentication, and scaling to fit your business needs.

References