Azure Diagram MCP Server
by Panth-Shah
README.md
# Azure Diagram MCP Server
A remotely-hosted [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that turns agent-written architecture descriptions into Azure diagrams. Agents describe **topology** (services, groups, connections); the server owns **layout and styling** so output stays consistent across models and prompts.
Built for **Streamable HTTP** so any MCP client (Cursor, VS Code, Copilot, custom agents) can connect over the network. Diagrams are returned inline as PNG and can be persisted to **Azure Blob Storage** with a shareable SAS URL.
## Features
- **MCP tools** for diagram generation, Azure node discovery, and worked examples
- **Server-side style profiles** — Graphviz layout, fonts, cluster boxes, and connector colors are injected at render time; agents do not hard-code `graph_attr` or colors
- **Safe execution** of agent-supplied Python via AST validation, process isolation, restricted builtins, and a render timeout
- **CAF-aligned visual language** — default `caf-default` profile matches Microsoft reference-architecture styling (grey cluster boxes, dashed private paths, dotted observability links)
- **Optional Blob persistence** — inline PNG always returned; shareable URL when storage is configured
- **Docker-ready** — single image with Graphviz, Cairo, and Node.js (for optional Azure best-practice validation)
- **Azure Container Apps deployment** — Bicep + [Azure Developer CLI (`azd`)](https://learn.microsoft.com/azure/developer/azure-developer-cli/) template included
## How it works
```mermaid
flowchart LR
Agent["MCP client / LLM"] -->|"POST /mcp"| Server["FastMCP server"]
Server --> Validate["AST safety check"]
Validate --> Style["Inject style profile"]
Style --> Render["Graphviz → PNG"]
Render --> Inline["Inline PNG response"]
Render --> Blob["Azure Blob (optional)"]
```
1. The agent calls `list_azure_components` and `get_diagram_example` to learn valid `diagrams.azure.*` imports.
2. The agent writes **topology-only** Python using the [`diagrams`](https://diagrams.mingrammer.com) DSL and calls `generate_diagram`.
3. The server validates the code, injects the active style profile, renders in an isolated subprocess, and returns the PNG.
> **Note:** A second pipeline for declarative YAML/JSON reference architectures (`generate_reference_diagram`) exists in the codebase but is **currently disabled** in the MCP server. All diagram generation today goes through `generate_diagram`.
## MCP tools
| Tool | Description |
| --- | --- |
| `generate_diagram(code, filename?, title?, style_profile?)` | Render `diagrams` DSL to PNG. Style/layout come from `style_profile` (default `caf-default`). |
| `list_azure_components(category?)` | List importable `diagrams.azure.*` node classes (`compute`, `network`, `database`, …). |
| `get_diagram_example()` | Return a minimal worked DSL example (topology only, no styling). |
| `list_style_profiles()` | List bundled layout/style presets and the default profile name. |
## Authoring diagrams (for agents)
**Do:** describe nodes, `Cluster` groupings, and connections.
**Do not:** set `graph_attr`, `direction`, `show`, `filename`, or arbitrary edge colors — the server applies those from the style profile.
```python
from diagrams import Diagram, Cluster, Edge
from diagrams.azure.network import ApplicationGateway
from diagrams.azure.web import AppServices
from diagrams.azure.security import KeyVaults
from diagrams.azure.database import SQLDatabases
with Diagram("Azure Web App"):
agw = ApplicationGateway("App Gateway")
with Cluster("Application"):
app = AppServices("App Service")
kv = KeyVaults("Key Vault")
db = SQLDatabases("SQL Database")
agw >> Edge(label="HTTPS") >> app
app >> kv
app >> Edge(style="dashed") >> db # dashed = indirect / private path
```
Connector semantics (colors come from the profile):
| `Edge(...)` | Meaning |
| --- | --- |
| plain `>>` | Solid data-flow connector |
| `Edge(style="dashed")` | Indirect, egress, or private path |
| `Edge(style="dotted")` | Observability, identity, or governance |
| `Edge(label="HTTPS")` | Label only — no custom colors needed |
## Style profiles
Profiles live under `src/azure_diagram_mcp/assets/style/`. The default **`caf-default`** profile defines:
- Top-to-bottom layout (`direction: TB`), spline routing, spacing
- Light grey dashed cluster boxes (VNet / subscription groupings)
- CAF connector palette (solid / dashed / dotted)
To tune diagram appearance globally, edit `caf-default.yaml` under the `diagrams_dsl` section — no agent prompt changes required.
```yaml
# excerpt from assets/style/caf-default.yaml
diagrams_dsl:
direction: TB
graph_attr:
fontsize: "22"
bgcolor: white
splines: spline
connectors:
solid: { style: solid, color: "#323130" }
dashed: { style: dashed, color: "#605E5C" }
dotted: { style: dotted, color: "#A19F9D" }
```
Pass a different profile to `generate_diagram(..., style_profile="caf-default")`.
## Security
Executing agent-supplied Python is the primary risk. Mitigations:
| Layer | What it does |
| --- | --- |
| **AST allowlist** (`validation.py`) | Only `diagrams` imports permitted; dangerous builtins and dunder escapes rejected |
| **Process isolation** (`renderer.py`) | Code runs in a separate process with `open` / `eval` / `exec` stripped from builtins |
| **Timeout** | Wall-clock limit on each render (default 30 s) |
| **Style injection** | Server overrides layout kwargs — agents cannot control filenames or open arbitrary files via `Diagram` |
For production, also restrict Container Apps ingress (authentication, IP allow lists).
## Quick start
### Prerequisites
- **Python 3.12+**
- **Graphviz** (`dot` on `PATH`)
- Windows: `winget install graphviz`
- macOS: `brew install graphviz`
- Linux: `apt-get install graphviz`
### Run locally (Python)
```bash
python -m venv .venv
# Windows: .venv\Scripts\activate
# macOS/Linux: source .venv/bin/activate
pip install -e .
python -m azure_diagram_mcp.server
```
Server endpoints:
| URL | Purpose |
| --- | --- |
| `http://localhost:8000/mcp` | MCP Streamable HTTP transport |
| `http://localhost:8000/health` | Liveness probe (`{"status":"ok","version":"0.1.0"}`) |
### Run locally (Docker)
```bash
docker build -t azure-diagram-mcp:local .
docker run -d --name azure-diagram-mcp-local -p 8000:8000 --restart unless-stopped azure-diagram-mcp:local
```
Rebuild and redeploy after code changes:
```bash
docker build -t azure-diagram-mcp:local .
docker rm -f azure-diagram-mcp-local
docker run -d --name azure-diagram-mcp-local -p 8000:8000 --restart unless-stopped azure-diagram-mcp:local
```
### Connect an MCP client
Point your client at the `/mcp` URL. Example for Cursor / VS Code (`.cursor/mcp.json` or `.vscode/mcp.json`):
```json
{
"mcpServers": {
"azure-diagram": {
"url": "http://localhost:8000/mcp"
}
}
}
```
For a deployed Container App, replace the host with your app FQDN: `https://<fqdn>/mcp`.
### Optional: Blob persistence
Without storage, diagrams are returned inline only. For shareable URLs locally:
```powershell
# Windows PowerShell
$env:AZURE_STORAGE_CONNECTION_STRING = "<connection-string>"
$env:BLOB_CONTAINER = "diagrams"
```
In Azure (Container Apps deployment), the app uses **managed identity** and user-delegation SAS — no connection string required.
## Deploy to Azure Container Apps
Prerequisites: [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd), Docker, Azure subscription.
```bash
azd auth login
azd up # first-time: provision infra + deploy
azd deploy # subsequent code/image updates
azd down # tear down
```
`azd up` provisions (via [`infra/`](infra/)):
- Log Analytics + Container Apps environment
- Azure Container Registry
- User-assigned managed identity (ACR pull + Storage Blob Data Contributor)
- Storage account + `diagrams` container
- Container App with external ingress on port 8000
When complete, azd prints `SERVICE_MCP_ENDPOINT_URL`. Your MCP endpoint is that URL + `/mcp`.
| Variable | Purpose |
| --- | --- |
| `STORAGE_ACCOUNT_URL` | Blob endpoint (managed identity) |
| `BLOB_CONTAINER` | Container name (`diagrams`) |
| `AZURE_CLIENT_ID` | User-assigned identity client ID |
| `PORT` | Listen port (`8000`) |
## Project layout
```
src/azure_diagram_mcp/
server.py # FastMCP app, MCP tools, /health
renderer.py # Isolated subprocess render + style injection
dsl_style.py # Style profile loader for diagrams DSL
validation.py # AST safety allowlist
catalog.py # diagrams.azure.* node discovery
storage.py # Blob upload + SAS URLs
assets/
style/ # Style profiles (caf-default.yaml, …)
icon_map.yaml # Service metadata (reference pipeline)
renderers/ # CAF layout engine (reference pipeline, currently disabled)
examples/ # Sample reference-architecture YAML
infra/ # Bicep (Container Apps, ACR, Storage, identity)
Dockerfile
azure.yaml # azd service definition
```
## Environment variables
| Variable | Default | Description |
| --- | --- | --- |
| `PORT` | `8000` | HTTP listen port |
| `HOST` | `0.0.0.0` | Bind address |
| `LOG_LEVEL` | `INFO` | Python log level |
| `AZURE_STORAGE_CONNECTION_STRING` | — | Local/dev Blob storage |
| `STORAGE_ACCOUNT_URL` | — | Blob endpoint (Azure managed identity) |
| `BLOB_CONTAINER` | `diagrams` | Blob container name |
| `AZURE_CLIENT_ID` | — | Managed identity client ID |
| `DISABLE_AZURE_MCP` | — | Set to `1` to skip external best-practice calls |
## License
MIT — see [LICENSE](LICENSE).
## Acknowledgements
Inspired by David Minkovski's [Stop Drawing, Start Prompting](https://www.davidminkovski.com/blog/stop-drawing-start-prompting-architecture-diagrams-the-easy-way) and his stdio [`azure-diagram-mcp`](https://github.com/dminkovski/azure-diagram-mcp). This project adapts the idea for **remote Streamable HTTP hosting**, **server-side style profiles**, and **durable Blob storage**.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues