
MCP series: Je eerste MCP-server bouwen
Deel 1, je eerste MCP-server bouwen
Deze blogpost is de tweede in een reeks over het bouwen van je MCP-server, het hosten ervan op Azure en tot slot het implementeren ervan in een M365 Copilot-agent.
In deel één bouwden we een volledig functionele MCP-server in gewone JavaScript die drie projectbeheertools blootstelt en zowel STDIO (voor lokale ontwikkeling) als HTTP (voor externe clients) ondersteunt. Nu is het tijd om hem van je laptop naar Azure te brengen.
Azure Container Apps is de ideale keuze voor het hosten van een MCP-server: serverless, schaalt naar nul, HTTPS out of the box en eenvoudig te deployen.
Note
Deze stap vereist Docker Desktop. Download en installeer het via docker.com/products/docker-desktop. Na installatie start je Docker Desktop en wacht je tot het walvispictogram in de systeembalk “Docker Desktop is running” toont voordat je docker-commando’s uitvoert.
Maak een Dockerfile aan in de projectroot:
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js .
EXPOSE 3000
CMD ["node", "server.js"]
Maak een .dockerignore aan:
node_modules
.env
Lokaal bouwen en testen:
docker build -t my-mcp-server .
docker run -p 3000:3000 my-mcp-server
Note
Zorg dat de Azure CLI geïnstalleerd is. Download het via learn.microsoft.com/cli/azure/install-azure-cli en voer az login uit voordat je verdergaat.
ACR-namen zijn wereldwijd uniek in heel Azure. Als az acr create een AlreadyInUse-fout geeft, is die naam al in gebruik. Kies iets onderscheidends: voeg je naam, bedrijfsnaam of een willekeurig achtervoegsel toe, bijv. acrmcpnico.
$RESOURCE_GROUP = "rg-mcp-demo"
$LOCATION = "westeurope"
$ACR_NAME = "acrmcpyourname" # Moet wereldwijd uniek zijn in Azure, kies iets onderscheidends
$APP_ENV = "mcp-env"
$APP_NAME = "mcp-project-server"
az group create --name $RESOURCE_GROUP --location $LOCATION
# Registreer de Container Registry-provider indien nog niet gedaan
az provider register --namespace Microsoft.ContainerRegistry
# Wacht tot de registratie voltooid is voordat je verdergaat
az provider show --namespace Microsoft.ContainerRegistry --query "registrationState"
# Herhaal het bovenstaande commando totdat je "Registered" ziet
az acr create `
--resource-group $RESOURCE_GROUP `
--name $ACR_NAME `
--sku Basic `
--admin-enabled true
az acr login --name $ACR_NAME
docker tag my-mcp-server "$ACR_NAME.azurecr.io/my-mcp-server:latest"
docker push "$ACR_NAME.azurecr.io/my-mcp-server:latest"
az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights
az containerapp env create `
--name $APP_ENV `
--resource-group $RESOURCE_GROUP `
--location $LOCATION
$ACR_PASSWORD = az acr credential show `
--name $ACR_NAME `
--query "passwords[0].value" -o tsv
az containerapp create `
--name $APP_NAME `
--resource-group $RESOURCE_GROUP `
--environment $APP_ENV `
--image "$ACR_NAME.azurecr.io/my-mcp-server:latest" `
--registry-server "$ACR_NAME.azurecr.io" `
--registry-username $ACR_NAME `
--registry-password $ACR_PASSWORD `
--target-port 3000 `
--ingress external `
--min-replicas 0 `
--max-replicas 3 `
--cpu 0.25 `
--memory 0.5Gi `
--env-vars NODE_ENV=production
Haal je publieke URL op:
az containerapp show `
--name $APP_NAME `
--resource-group $RESOURCE_GROUP `
--query "properties.configuration.ingress.fqdn" -o tsv
Je MCP-endpoint is nu live op https://<your-fqdn>/mcp.
Note
Het /mcp-endpoint accepteert enkel POST-verzoeken. Als je de URL in een browser opent, wordt een GET-verzoek verstuurd en krijg je Cannot GET /mcp terug — dit is normaal. Gebruik het /health-endpoint om te controleren of de container actief is: https://<your-fqdn>/health zou {"status":"ok"} moeten teruggeven.
Om het MCP-endpoint zelf te testen, gebruik je een API-client zoals Bruno (open source) of Postman.
Maak een nieuw POST-verzoek aan met de volgende instellingen:
https://<your-fqdn>/mcpContent-Type: application/jsonAccept: application/json, text/event-stream{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
Een geslaagde respons geeft je drie tools terug in SSE-formaat. Je kunt ook een specifieke tool aanroepen:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "listProjects",
"arguments": {}
}
}

Standaard is je MCP-endpoint publiek toegankelijk — iedereen met de URL kan je tools aanroepen. Voor een echte productie-omgeving wil je het minimaal beveiligen met een API-sleutel.
Voeg de API-sleutelcontrole toe aan de MCP-route. De sleutel wordt gelezen uit een omgevingsvariabele zodat hij nooit hardgecodeerd in je broncode terechtkomt:
app.post("/mcp", async (req, res) => {
const apiKey = req.headers["x-api-key"];
if (!apiKey || apiKey !== process.env.MCP_API_KEY) {
res.status(401).json({ error: "Unauthorized" });
return;
}
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
res.on("finish", () => transport.close());
});
Maak een .env-bestand aan in je projectroot (voeg het toe aan .gitignore!):
MCP_API_KEY=my-local-secret
Installeer dotenv om het automatisch te laden:
npm install dotenv
Voeg dit toe bovenaan server.js:
import "dotenv/config";
Herstart de server en verifieer dat authenticatie werkt:
Zonder sleutel: zou 401 moeten teruggeven:
curl -N -X POST http://localhost:3000/mcp -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Met de juiste sleutel: zou de toollijst moeten teruggeven:
curl -N -X POST http://localhost:3000/mcp -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "x-api-key: my-local-secret" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Voeg in Bruno een header toe aan je verzoek:
| Header | Waarde |
|---|---|
x-api-key | my-local-secret |

Geef geheimen nooit door als gewone omgevingsvariabelen. Gebruik in plaats daarvan de ingebouwde geheimopslag van Container Apps:
az containerapp secret set `
--name $APP_NAME `
--resource-group $RESOURCE_GROUP `
--secrets mcp-api-key=<your-production-secret>
az containerapp update `
--name $APP_NAME `
--resource-group $RESOURCE_GROUP `
--set-env-vars MCP_API_KEY=secretref:mcp-api-key
Het geheim wordt versleuteld opgeslagen in Azure en tijdens runtime in de container geïnjecteerd — het verschijnt nooit in deployment-logs of in de uitvoer van az containerapp show.
Note
Overweeg voor enterprise-scenario’s om Azure API Management voor je Container App te plaatsen. APIM biedt OAuth2/Entra ID-authenticatie, rate limiting en volledige observabiliteit — allemaal zonder je servercode te wijzigen.