Skip to main content
Glama
sinanpl

mcp-demo-aad-viz

by sinanpl

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.

The interactive Altair chart builder widget, showing dataset/axis/mark controls next to a rendered scatter plot of the Palmer penguins dataset


Try it without Azure

No tenant, no auth, no deployment — enough to see the widget work:

uv sync && uv run python scripts/fetch_datasets.py
MCP_DATAVIZ_AUTH_ENABLED=false MCP_DATAVIZ_PORT=3001 uv run python -m mcp_dataviz

Every 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 v1

The 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 --tag on 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)

auth.py

Group membership changes the size of the catalogue

MCP Apps (io.modelcontextprotocol/ui)

chart_builder.html

Dropdowns re-render the chart in place

App-only tools (visibility: ["app"])

render_chart

Widget re-renders cost zero tokens

input_required

plot_dataset

Hosts without widgets get a form instead

Scope step-up (403 insufficient_scope)

export_chart

First export triggers re-consent

Resources + templates

data://catalog

Permission-filtered

Completions

dataset arguments

Autocomplete never names a dataset you can't open

Prompts

explore_dataset

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

Datasets.Open

iris, penguins, cars, barley

operations

Datasets.Operations

seattle-weather, us-employment, gapminder

confidential

Datasets.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:

  1. plot_dataset returns a ~900-byte handle — encoding, row counts, warnings. No spec.

  2. The host renders the ui:// app and pushes it that handle.

  3. 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:8765

It 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:8080

This 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)

iris

seattle-weather

diamonds — unit pricing

penguins

us-employment

movies — commercial revenue

cars

gapminder

titanic — person-level records

barley


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-COMPATIBILITY

The 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 needed
uv run ruff check src tests scripts

tests/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.sh

Licence

MIT.

-
license - not tested
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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.

View all MCP Connectors

Latest Blog Posts

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