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.
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 | shon macOS/Linux, orpowershell -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:
[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
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 codeDockerfile– 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.
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)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.
-
Start the server in a terminal:
uv run main.pyYou should see Uvicorn running on http://0.0.0.0:8000.
-
Launch the inspector in a second terminal:
fastmcp dev main.pyA page opens at
http://127.0.0.1:5173and automatically connects to your server. -
Use the inspector to test list_currencies – you should see a list of currency codes and names.
-
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
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"}.
Azure Container Apps automatically provisions an HTTPS certificate when ingress is set to external.
Connecting the MCP server to Copilot Studio
- Go to Copilot Studio.
- Create a new agent or open an existing one.
- Navigate to Settings (gear icon) → AI tools → MCP servers.
- Click Add MCP server.
- 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).
- Click Save. Copilot Studio attempts to connect. If successful, you’ll see the two tools (
list_currenciesandconvert_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-varsor 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.AsyncClientacross 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
asyncso other requests are not held up. - No health endpoint: Azure Container Apps pings
/healthto determine container readiness. Without it, the container may be repeatedly restarted. - Wrong port: Verify the container exposes port
8000and that--target-port 8000is 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
- Original source article: Create An MCP Server And Deploy To Copilot Studio by Matthew Devaney
- FastMCP documentation: https://fastmcp.ai
- Frankfurter Exchange Rate API: https://www.frankfurter.app
- Microsoft Learn: Build your own MCP server for Copilot Studio (topic-specific page pending)
- Azure Container Apps overview: https://learn.microsoft.com/en-us/azure/container-apps/