Two containers. One agent conversation.
A Python MCP server and a Microsoft Agent Framework (.NET) client wired together with Docker Compose — showing how to compose agents with remote tools over real service-to-service networking.
┌──────────────────────┐ Streamable HTTP /mcp ┌──────────────────────┐
│ agent-client (.NET) │ ─────────────────────▶ │ mcp-server (Python) │
│ Microsoft.Agents.AI │ JSON-RPC │ FastMCP │
│ + Azure OpenAI │ │ list/add/complete │
└──────────────────────┘ └──────────────────────┘
│ │
└─────────── mcp-net (bridge network) ────────────┘
Part of a Docker-first series for Microsoft Agent Framework:
agent-framework-devcontainer·mcp-docker-starter·ai-agents-compose-stack
| Pattern shown | Where to look |
|---|---|
| Polyglot compose (Python + .NET) | compose.yaml |
| Service-to-service via bridge network | networks: mcp-net |
| Service discovery by name | Client connects to http://mcp-server:8000/mcp |
| Non-root containers | Both Dockerfiles use dedicated system users |
| Healthcheck on the MCP server | mcp-server/Dockerfile |
| Multi-stage .NET build, Alpine runtime | agent-client/Dockerfile |
| Client readiness wait | WaitForEndpointAsync in Program.cs |
Secrets via .env, never committed |
.gitignore + .dockerignore |
- Docker Desktop (or Docker Engine + Compose v2)
- An Azure OpenAI resource with a chat deployment (e.g.
gpt-4o-mini)
Both components ship as multi-arch images (linux/amd64 + linux/arm64) with SBOM and build provenance:
| Image | Package |
|---|---|
ghcr.io/ppiova/mcp-docker-starter/mcp-server:latest |
view |
ghcr.io/ppiova/mcp-docker-starter/agent-client:latest |
view |
Drop this compose.ghcr.yaml next to your .env and skip the local build entirely:
services:
mcp-server:
image: ghcr.io/ppiova/mcp-docker-starter/mcp-server:latest
ports: ["8000:8000"]
networks: [mcp-net]
agent-client:
image: ghcr.io/ppiova/mcp-docker-starter/agent-client:latest
depends_on: [mcp-server]
env_file: [.env]
environment:
MCP_SERVER_URL: "http://mcp-server:8000/mcp"
networks: [mcp-net]
stdin_open: true
tty: true
networks: { mcp-net: {} }docker compose -f compose.ghcr.yaml upgit clone https://github.com/ppiova/mcp-docker-starter.git
cd mcp-docker-starter
cp .env.example .env
# edit .env with your Azure OpenAI values
docker compose up --buildYou should see (abbreviated):
mcp-tasks-server | [tasks-mcp] SSE listening on http://0.0.0.0:8000/mcp
mcp-agent-client | ✅ MCP host reachable at http://mcp-server:8000/
mcp-agent-client | 🔌 Connecting to MCP: http://mcp-server:8000/mcp
mcp-agent-client | 🧰 MCP tools discovered: list_tasks, add_task, complete_task, stats
mcp-agent-client | > Prompt: Mostrame el estado actual de tareas...
mcp-agent-client | --- Respuesta (streaming) ---
mcp-agent-client | Tareas abiertas:
mcp-agent-client | 1. Review Agent Framework samples (high)
mcp-agent-client | 2. Write blog post about MCP (medium)
mcp-agent-client | ...
docker compose run --rm agent-client "Completá la tarea 1 y mostrame las stats"mcp-server/server.py uses FastMCP (Python) to expose 4 tools:
| Tool | Args | Returns |
|---|---|---|
list_tasks |
status? = 'open' | 'done' |
array of tasks |
add_task |
title: str, priority: 'low'|'medium'|'high' |
new task |
complete_task |
id: int |
updated task |
stats |
— | counters + priority breakdown |
State is in-memory on purpose — it's a starter. Swap for Redis/SQL when you need persistence (add a redis service to compose.yaml).
It's exposed on localhost:8000 for convenience. Use the official MCP Inspector:
npx @modelcontextprotocol/inspector
# Then connect to http://localhost:8000/mcp (transport: streamable-http)Note on DNS-rebinding protection — the Python MCP SDK rejects unknown Host headers by default. Inside Docker Compose the client reaches the server via the service name (
mcp-server:8000), so we explicitly allow it viaTransportSecuritySettings(allowed_hosts=...). If you deploy behind a reverse proxy / ingress, add the extra hostnames through theMCP_ALLOWED_HOSTSenv var (comma-separated).
agent-client/Program.cs (C#, .NET 8) does four things:
- Waits for the MCP host to be reachable (compose
depends_on+ app-level readiness check). - Opens an SSE MCP connection via
ModelContextProtocolclient SDK. - Lists tools from the MCP server and casts them into
AITools. - Creates an
AIAgentviaMicrosoft.Agents.AIbound to Azure OpenAI, with the MCP tools attached.
The model decides which MCP tools to call based on the prompt. No mock, no handwritten wrappers — MCP tools flow straight into the Agent Framework.
- Add persistence — swap the in-memory dict for Redis/SQLite and add a
redis/dbservice. - Add more MCP tools — decorate with
@mcp.tool(), they're auto-discovered by the client. - Scale the client — run N agent-client replicas against a single MCP server.
- Production hardening — put the MCP server behind a reverse proxy, enable auth, ship traces via OpenTelemetry (see
ai-agents-compose-stack). - Swap models — point the client at OpenAI.com or Ollama using
Microsoft.Extensions.AIproviders.
.
├── .devcontainer/
│ └── devcontainer.json
├── agent-client/
│ ├── Dockerfile # multi-stage, Alpine runtime, non-root
│ ├── AgentClient.csproj # .NET 8 + Microsoft.Agents.AI + ModelContextProtocol
│ └── Program.cs # Connects to MCP, creates agent, streams response
├── mcp-server/
│ ├── Dockerfile # Python 3.12-slim, non-root, healthcheck
│ ├── requirements.txt
│ └── server.py # FastMCP + 4 tools + SSE transport
├── compose.yaml # Two services on a private bridge network
├── .dockerignore
├── .env.example
├── .gitignore
├── LICENSE
└── README.md
Same pattern as the other repos in this series:
AZURE_OPENAI_API_KEYif set → key auth (simplest inside Docker).- Otherwise →
AzureCliCredential(works in the Dev Container afteraz login). - Otherwise →
DefaultAzureCredential(Managed Identity / env vars in production).
MIT — by Pablo Piovano · Microsoft MVP in AI.