Azure Functions (Python) app that renders matplotlib charts and returns a public URL to the PNG. Exposes the same renderer through two surfaces:
- HTTP:
POST /api/chart— usable from Copilot Studio, Power Automate, curl, anything. - MCP: a single tool
generate_charton an MCP server namedMatplotlibChartGenerator, hosted at/runtime/webhooks/mcp.
The MCP tool is designed to be reasoning-driven: it requires the caller to articulate intent, takeaway, and audience before picking chart type and styling, so the resulting chart's annotations actually support the conclusion the viewer should reach.
- Chart types: bar, line, pie, scatter, histogram, area, box, violin, heatmap
- Multi-series support (all types except pie and heatmap)
- Dual y-axes for line/scatter/bar when series have different units
- Error bars (bar/line), stacked variants (bar/area), horizontal bars
- Log scales, explicit axis limits
- Six annotation types:
point,hline,vline,hspan,vspan,text - Styling: matplotlib themes (
ggplot,seaborn-v0_8,dark_background,fivethirtyeight,bmh,grayscale, default), custom fonts, figsize, dpi, grid
Rendered PNGs are uploaded to Azure Blob Storage and the public URL is returned.
client ──► /api/chart ─┐
├─► render with matplotlib ─► upload PNG to Blob ─► return public URL
MCP client ──► generate_chart ─┘
Both entry points share the same renderer in function_app.py.
function_app.py Single-file Functions app: HTTP route + MCP tool + renderer
host.json Functions host config (extension bundle, MCP server config)
requirements.txt Python dependencies
openapi.json OpenAPI 2.0 spec for the /api/chart HTTP endpoint
local.settings.json.example Template for local env config — copy to local.settings.json
copilot-studio-instructions.md Setup guide for using this as a Copilot Studio action
topics/ Copilot Studio topic YAMLs (prepare-chart-data, render-chart)
The app reads these environment variables (locally via local.settings.json, in Azure via App Settings):
| Variable | Required | Default | Purpose |
|---|---|---|---|
AzureWebJobsStorage |
yes | — | Functions runtime storage (can be UseDevelopmentStorage=true locally) |
FUNCTIONS_WORKER_RUNTIME |
yes | python |
Functions worker language |
CHARTS_BLOB_CONNECTION_STRING |
yes | — | Connection string for the storage account where rendered chart PNGs are uploaded |
CHARTS_BLOB_CONTAINER |
no | matplotlib-charts |
Blob container name (must be publicly readable for the returned URL to work) |
⚠️ The blob container needs public-read access for the returned URL to render in browsers/Copilot. Either set the container's anonymous access level to "Blob", or replace_upload_to_blobwith a SAS-based variant if you want to keep it private.
Requires Python 3.10+ and the Azure Functions Core Tools v4.
# 1. Clone & set up venv
git clone https://github.com/<YOUR-GH-USER>/matplotlib-azurefunction.git
cd matplotlib-azurefunction
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS/Linux
pip install -r requirements.txt
# 2. Copy the example settings and fill in your storage connection string
copy local.settings.json.example local.settings.json
# edit local.settings.json — set CHARTS_BLOB_CONNECTION_STRING
# 3. Run
func startThe HTTP endpoint will be at http://localhost:7071/api/chart. The MCP server is reachable at http://localhost:7071/runtime/webhooks/mcp.
curl -X POST http://localhost:7071/api/chart \
-H "Content-Type: application/json" \
-d '{
"chart_type": "bar",
"data": {"labels": ["A","B","C"], "values": [10, 20, 15]},
"params": {"title": "Smoke test", "style": "ggplot"}
}'Provision once:
# Resource group, storage account, function app (Linux, Python 3.11, Flex Consumption or Consumption)
az group create --name <rg> --location <region>
az storage account create --name <storage> --resource-group <rg> --sku Standard_LRS
az functionapp create \
--resource-group <rg> \
--name <app> \
--storage-account <storage> \
--runtime python --runtime-version 3.11 \
--functions-version 4 \
--os-type Linux --consumption-plan-location <region>
# Configure app settings — the storage connection string the renderer uploads to
az functionapp config appsettings set \
--resource-group <rg> --name <app> \
--settings \
CHARTS_BLOB_CONNECTION_STRING="<connection-string>" \
CHARTS_BLOB_CONTAINER="matplotlib-charts"Deploy via zip (recommended for this repo — func azure functionapp publish --build remote has been observed to silently fail here):
# From the project root, with the venv activated
python -m pip install --target .python_packages\lib\site-packages -r requirements.txt
Compress-Archive -Path .\* -DestinationPath deploy.zip -Force
az functionapp deployment source config-zip \
--resource-group <rg> --name <app> --src deploy.zipPOST /api/chart?code=<function-key> — request body matches the MCP tool's chart_type / data / params shape. See openapi.json for the full schema.
Function-level auth is enforced (AuthLevel.FUNCTION) — each caller should get its own named key:
az functionapp keys set \
--resource-group <rg> --name <app> \
--key-name <caller-name> --key-type functionKeysTool name: generate_chart. Required arguments: intent, takeaway, audience, chart_type, data, params. The first three are reasoning notes — the server doesn't act on them, they exist to force the calling model to think before parameterizing the chart. See the tool description in function_app.py for full property docs.
See copilot-studio-instructions.md and the topic YAMLs in topics/.
MIT