mcp-demo-aad-viz
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-demo-aad-vizBuild an interactive scatter plot of the penguins dataset"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-demo-aad-viz
A worked example of two MCP capabilities that are usually demonstrated in isolation, and are considerably more interesting together:
Microsoft Entra ID (Azure AD) authorization — the server is an OAuth 2.1 resource server. Your Entra group membership decides which datasets exist for you. Not "listed then refused" — absent.
Inline apps / extensions (
io.modelcontextprotocol/ui) — charts arrive as an interactive widget rendered in the conversation, and adjusting it costs zero tokens.
The two combine into the thing worth seeing: an Altair chart builder whose dataset dropdown contains exactly the datasets your Entra groups permit, enforced server-side on every single interaction with the widget.
Built against MCP 2026-07-28 with the Python SDK mcp 2.0. Deploys to Azure
Container Apps. MIT licensed.
Heads up: this is a demonstration, not a product. It ships ten public sample datasets and a deliberately simple tier model so the authorization story is legible.

Try it without Azure
No tenant, no auth, no deployment — enough to see the widget work:
uv sync && uv run python scripts/fetch_datasets.pyMCP_DATAVIZ_AUTH_ENABLED=false MCP_DATAVIZ_PORT=3001 uv run python -m mcp_datavizEvery caller is then treated as holding all three dataset tiers. Point any MCP Apps
host at http://localhost:3001/mcp — see Local development for
a browser-based host that shows you the whole ui/ protocol as it happens.
Try it with Azure
# 1. Directory objects (app registration, scopes, app roles, 3 groups)
./scripts/entra-setup.sh# 2. Put yourself in a group to pick a persona
source entra.env
az ad group member add --group "$MCP_DATAVIZ_GROUP_ANALYSTS_ID" \
--member-id "$(az ad signed-in-user show --query id -o tsv)"# 3. Deploy (builds the image in Azure; no local Docker needed)
./scripts/deploy.sh --tag v1The script prints your MCP endpoint. Add it to your client exactly as printed —
the /mcp path is part of the OAuth resource identifier → docs/CONNECT.md.
Pass a unique
--tagon every deploy. With a repeated tag the Bicep template is byte-identical to the live one, no new revision is created, and the deploy reports success while shipping nothing.
What it demonstrates
MCP feature | Where | What you see |
Authorization (OAuth 2.1 RS) | Group membership changes the size of the catalogue | |
MCP Apps ( | Dropdowns re-render the chart in place | |
App-only tools ( |
| Widget re-renders cost zero tokens |
|
| Hosts without widgets get a form instead |
Scope step-up ( |
| First export triggers re-consent |
Resources + templates |
| Permission-filtered |
Completions | dataset arguments | Autocomplete never names a dataset you can't open |
Prompts |
| Guided first pass |
Two things the spec deprecated in this revision, and which this server therefore
avoids: sampling and the logging capability (SEP-2577). suggest_chart picks a
mark from column types instead of asking a model.
The authorization model
Two independent axes. Getting these confused is the usual mistake.
WHO YOU ARE WHAT YOU'RE DOING
Entra group ──► app role ──► dataset tier OAuth scope ──► operation
(roles claim) (scp claim)
analysts → Open ( 4) Datasets.Read → everything
engineers → Open + Operations ( 7) Datasets.Export → export_chart
scientists → Open + Confidential ( 7) ↑ withheld at first, so the
...a *different* 7 first export triggers a step-up
(no group) → nothing ( 0)Tier | Role | Datasets |
open |
| iris, penguins, cars, barley |
operations |
| seattle-weather, us-employment, gapminder |
confidential |
| diamonds, movies, titanic |
Engineers and scientists hold the same number of datasets but not the same ones, so two colleagues asking the same question get different answers.
./scripts/assign-persona.sh engineer colleague@example.com --now--now also assigns the roles directly to the user: a group change can take Entra
several minutes to reach a new token, a direct assignment about twenty seconds.
Roles are a hard denial. You cannot request your way into a group. Datasets
outside your tier are absent from tools/list results, resources/list,
completions and the widget's dropdown — not listed-then-refused.
Scopes are a soft denial. Missing Datasets.Export returns 403 with a
WWW-Authenticate: Bearer error="insufficient_scope" challenge, and the client
re-authorizes asking for it.
Two Entra-specific traps this repo works around, both of which produce baffling
errors if you set it up by hand: Entra has no dynamic client registration and no
RFC 8414 metadata endpoint, and the MCP URL must be registered as an Application
ID URI or RFC 8707 resource= fails with AADSTS9010010.
Full detail: docs/AUTHZ.md · docs/CONNECT.md.
Why the widget is interesting
A Vega-Lite spec with inline data is 30–300 KB. Naively returning it from a tool puts that in the model's context on every chart.
Instead:
plot_datasetreturns a ~900-byte handle — encoding, row counts, warnings. No spec.The host renders the
ui://app and pushes it that handle.The widget calls
render_chart(an app-only tool) for the actual spec.
Because step 3 originates in the app rather than the model, the spec never enters the conversation. Changing a dropdown is one small server round-trip and zero tokens.
The authorization story holds here too: render_chart and app_catalogue re-derive
the caller's tiers on every call, so the widget cannot reach a dataset the token does
not permit — even though the model is no longer in the loop.
Design notes: docs/DESIGN.md.
Local development
Two harnesses, for two different questions.
"Is my widget's HTML/JS right?" — a miniature host that speaks the real ui/
postMessage protocol and logs every message, with no MCP client involved:
uv run python scripts/preview_widget.py # http://127.0.0.1:8765It injects the same restrictive CSP a real host applies, so CSP failures reproduce
here instead of only in production. --strip-structured-content emulates a host
defect described in docs/HOST-COMPATIBILITY.md.
"Is my MCP surface right?" — the reference host from the MCP Apps repo, driving the actual server over HTTP:
git clone https://github.com/modelcontextprotocol/ext-apps.git
cd ext-apps && npm install && cd examples/basic-host
SERVERS='["http://localhost:3001/mcp"]' npm start # http://localhost:8080This is the harness worth reaching for first when a widget renders blank: it reports
protocol violations that shipping hosts swallow silently. Running the server with
MCP_DATAVIZ_AUTH_ENABLED=false also relaxes the SDK's Origin check and adds CORS
headers, which a browser-based host needs and which are off whenever auth is on.
Note that the widget HTML is read once at server construction, so editing it requires a server restart.
Host compatibility
MCP Apps support varies between hosts in ways that produce identical-looking symptoms — usually a blank or collapsed widget with no error anywhere. docs/HOST-COMPATIBILITY.md documents what was actually observed, how each cause was isolated, and which are fixable server-side (one of three) versus not.
Datasets
Four open, three operations, three confidential — all public sample datasets from the Vega datasets collection. Vendored into the image at build time, so the running container needs no network access to any data origin. The tier labels are illustrative, chosen to make the access model concrete.
open | operations | confidential (illustrative reason) |
|
|
|
|
|
|
|
|
|
|
Layout
src/mcp_dataviz/
server.py tools, resources, prompts, completions
auth.py Entra token verification, roles→tiers, scope challenge
catalog.py the 10 datasets and the tier gate
charts.py Altair → Vega-Lite, with aggregation pushed into pandas
config.py environment settings (nothing hardcoded)
widgets/ the MCP App
infra/ Bicep: ACR, Container Apps, Log Analytics
scripts/ entra-setup.sh, deploy.sh, assign-persona.sh, preview_widget.py
tests/ 168 tests, incl. HTTP-level auth and step-up
docs/ AUTHZ, CONNECT, DESIGN, HOST-COMPATIBILITYThe Python package keeps the name mcp_dataviz (and the MCP_DATAVIZ_ environment
prefix) even though the repository is mcp-demo-aad-viz; renaming it would churn every
Azure resource name and environment variable for no benefit.
Tests
uv run pytest # 168 tests, no Azure neededuv run ruff check src tests scriptstests/test_http.py runs a real uvicorn server and asserts the 401 challenge, the
PRM document, the 403 insufficient_scope step-up, and the input_required round trip.
Cost
Container Apps scales to zero (minReplicas: 0), so an idle demo costs roughly
nothing; ACR Basic and Log Analytics are the only standing charges (a few € / month).
az group delete --name rg-mcp-dataviz --yes && ./scripts/entra-teardown.shLicence
MIT.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
MCP Hub: AI service discovery, per-user OAuth, and multi-service workflow orchestration
Official Microsoft MCP Server to query Microsoft Entra data using natural language
Manage SRG+ hubs, channels, content, assets, users, and workspaces from any MCP-aware AI agent.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/sinanpl/mcp-demo-aad-viz'
If you have feedback or need assistance with the MCP directory API, please join our Discord server